Using KnockoutJs, SystemJs, ES2015, Jasmine and Karma with ASP.NET MVC 6 in Visual Studio 2015: The bleeding edge of Web Development
Or at least, a bleeding edge. One of many, for the Web is a fast-changing world.
This was originally a guest blog for KPIT Recruitment. If you want to read the original, written in Dutch, head to their site
A day away from the SPA
The main motivation for this post came from using Aurelia, a Single Page Application (SPA) framework that uses SystemJs as a module loader in combination with BabelJs as a transpiler, enabling you to work with ECMAScript 2015 today without worrying about browser support (or lack thereof, rather).
But of course, no matter how hip it's become, not every web application has to be a SPA (even though SPAs can have more than one page).
So at CodeNamed we wondered, how would all this work on a "regular", multi-paged MVC 6 application? And down the rabbit hole we went. This post is about us seeing the light at the end of said hole and wanting to document the steps in what has, at times, become a tortuous path so that you, dear reader, can walk in and out of it without a scratch. Until some of the packages we're going to use receive another update, that is. Welcome to the Web.
The setup
Let me start by saying that this post is not about how to use KnockoutJs, or how to write tests with Jasmine. It's neither about teaching you MVC 6 nor the new ES2015 syntax. Instead, this post is about getting all of these working together.
With that out of the way, open Visual Studio 2015 and...
- File / New Project
- Select the .NET Framweork 4.6.1 and ASP.NET Web Application
- From the ASP.NET 5 Templates, select Web Application
- Inside
wwwroot
, create ansrc
folder. This folder will hold our JavaScript sources. - Inside
src
, create aviewmodels
folder. - Inside
src
, create apages
folder. - One level up from
wwwroot
, create atest
folder.
The best way to install and work with SystemJs is to use jspm
.
jspm is a package manager for the SystemJS universal module loader, built on top of the dynamic ES6 module loader
Within a Command Prompt, go to the root of the project, which is one level above the wwwroot
folder, and install and initialize jspm
as follows:
npm install jspm
jspm init
The init
command will ask a series of questions. On most of those you can just press Enter to accept the default option. Just make sure that you set the baseUrl to ./wwwroot
and that you choose babel
as the transpiler.
Package.json file does not exist, create it? [yes]:
Would you like jspm to prefix the jspm package.json properties under jspm? [yes]:
Enter server baseURL (public folder path) [./]:./wwwroot
Enter jspm packages folder [wwwroot\jspm_packages]:
Enter config file path [wwwroot\config.js]:
Configuration file wwwroot\config.js doesn't exist, create it? [yes]:
Enter client baseURL (public folder URL) [/]:
Do you wish to use a transpiler? [yes]:
Which ES6 transpiler would you like to use, Babel, TypeScript or Traceur? [babel]:
ok Verified package.json at package.json
Verified config file at wwwroot\config.js
Looking up loader files...
system.js.map
system.src.js
system-csp-production.js
system-csp-production.src.js
system.js
system-polyfills.js.map
system-polyfills.js
system-polyfills.src.js
system-csp-production.js.map
Using loader versions:
systemjs@0.19.9
Looking up npm:babel-core
Looking up npm:babel-runtime
Looking up npm:core-js
Updating registry cache...
Looking up github:jspm/nodelibs-process
Looking up github:jspm/nodelibs-fs
Looking up github:jspm/nodelibs-path
Looking up github:systemjs/plugin-json
Looking up npm:process
Downloading npm:process@0.11.2
ok Installed github:jspm/nodelibs-fs@^0.1.0 (0.1.2)
ok Installed github:jspm/nodelibs-process@^0.1.0 (0.1.2)
ok Installed babel as npm:babel-core@^5.8.24 (5.8.34)
Looking up npm:path-browserify
ok Installed github:jspm/nodelibs-path@^0.1.0 (0.1.0)
ok Installed github:systemjs/plugin-json@^0.1.0 (0.1.0)
ok Installed npm:process@^0.11.0 (0.11.2)
ok Installed npm:path-browserify@0.0.0 (0.0.0)
Looking up github:jspm/nodelibs-assert
Looking up npm:assert
ok Installed github:jspm/nodelibs-assert@^0.1.0 (0.1.0)
Downloading npm:assert@1.3.0
Looking up npm:util
Downloading npm:util@0.10.3
Looking up npm:inherits
Downloading npm:inherits@2.0.1
ok Installed npm:assert@^1.3.0 (1.3.0)
ok Installed npm:util@0.10.3 (0.10.3)
ok Installed npm:inherits@2.0.1 (2.0.1)
Looking up github:jspm/nodelibs-util
ok Installed github:jspm/nodelibs-util@^0.1.0 (0.1.0)
ok Installed core-js as npm:core-js@^1.1.4 (1.2.6)
ok Installed babel-runtime as npm:babel-runtime@^5.8.24 (5.8.34)
ok Loader files downloaded successfully
This will create a jspm_packages
folder and a config.js
inside the wwwroot
folder, and a package.json
just outside of it.
Before we continue, we need to tell SystemJs where to look for our JavaScript files. Open config.js
and add the following line in the paths
configuration section:
"*": "src/*.js",
The whole section should look like this:
paths: {
"*": "src/*.js",
"github:*": "jspm_packages/github/*",
"npm:*": "jspm_packages/npm/*"
},
Likewise, locate the following line and remove it altogether from config.js
:
baseURL: "/",
We don't need to specify a baseUrl
for the application to work, and this setting would actually send Karma looking for files in the wrong place.
Time to install some packages
jspm
allows us to install packages from the npm
repository and even straight out of GitHub, which is very handy in case a package is not in the jspm
repo, as is the case with KnockoutJs and jQuery:
jspm install knockout=github:knockout/knockout
jspm install jquery=github:components/jquery
In both cases we tell jspm
to use an alias for these packages (jquery=
and knockout=
respectively). This alias will come in handy when we need to import the KnockoutJs and jQuery packages
Show me some code, already!
Ok, so how do we start using SystemJs and BabelJs?
1. _Layout.cshtml
Open the _Layout.cshtml
and add the following code just before the closing of the <head>
tag:
<script src="~/jspm_packages/system.js"></script>
<script src="~/config.js"></script>
<script type="text/javascript">
System.import('main');
</script>
Here we're telling SystemJS to load a module called main
. SystemJs will look inside the wwwroot/src
folder (as defined in config.js
) for a file called main.js
.
2. main.js
main.js
won't do much other than importing some of the modules that will be used throughout the application, like KnockoutJs or jQuery, so that we don't have to import them over and over in every module. Keep it DRY ;)
import $ from "jquery";
import ko from "knockout";
export class Main {
constructor () {
}
}
From this point on, the rest of the modules will have a central point to define the ko
and $
variables.
The idea is that every MVC View gets a module (a JavaScript file) with the same name (although it doesn't matter; it's just for clarity). These modules will be extremely simple and will have only three tasks:
- Import the
Main
module. - Import the KnockoutJs viewmodel for the current view, along with any other necessary modules (like, say, utility classes).
- Call
ko.applyBindings
to get the view up and running.
3. Index.cshtml
The first thing that Index.cshtml
needs to do, then, is import the module that will apply the KnockoutJs bindings (which we'll create in a minute), and of course, define some html.
To keep things simple, the view will only display a Name and Surname inputs and will show the concatenated full name below. Here's the view in its entirety:
<script type="text/javascript">
System.import('pages/index');
</script>
<h2>Give us your details</h2>
<div>
<div class="form-horizontal">
<div class="form-group">
<div class="col-lg-2">
<label>Name: </label>
</div>
<div class="col-lg-10">
<input type="text" class="form-control"
data-bind="textInput: name" />
</div>
</div>
<div class="form-group">
<div class="col-lg-2">
<label>Surame: </label>
</div>
<div class="col-lg-10">
<input type="text" class="form-control"
data-bind="textInput: surname" />
</div>
</div>
<div class="form-group">
<div class="col-lg-2">
<label>Full name: </label>
</div>
<div class="col-lg-10">
<span data-bind="text: fullName"></span>
</div>
</div>
</div>
</div>
As you can see, I have tried to apply some Bootstrap 3 classes and I've added Knockout bindings to the fields, but that's where the excitement ends.
4. fullName.js
Next up, we'll create the viewmodel that binds to the Index.cshtml view. I've placed it in the viewmodels
folder I created earlier within src
. This viewmodel is as simple as the view it binds to: it just defines the three properties that are needed in the view, and it all happens in the class's constructor.
export class FullName {
constructor() {
this.name = ko.observable("");
this.surname = ko.observable("");
this.fullName = ko.computed(() =>
`${this.name()} ${this.surname()}`);
}
}
5. index.js
Now, inside the pages
folder within src
, we'll add the module that Index.cshtml
imports in order to apply the KnockoutJs bindings.
import {Main} from "../main";
import {FullName} from "../viewmodels/fullName"
ko.applyBindings(new FullName());
You might be wondering what the point is of this "extra" file. Why not call ko.applyBindings(...)
in the viewmodel itself? Simple: because of unit testing. Having the call to applyBindings
on a separate file with virtually no code means that
- We won't need to test that file, and
- We won't need to worry about mocking the
ko
object on every spec.
In my first attempt I was actually calling ko.applybindings
inside the viewmodel itself, but the problem was that, because I'm importing KnockoutJs in the main
module and not in fullName
where it is actually used, the test runner kept throwing a "ko is not defined" exception; and importing KnockoutJs into the test file didn't help.
As it happens, I'm happier with this setup. It might seem overkill for a small demo application such as this one, but not having to import KnockoutJs in every file on a large application, where you will need it on pretty much every module, is actually quite a welcome idea.
Start me up
At this point, we have a functional application. Go ahead, run it; bask in the wonder of your creation.
But, I hear you say, how can we guarantee our code is stable when we haven't written any tests?
Prepping to test
Earlier we installed the jQuery and KnockoutJS packages. Next up are Jasmine and Karma.
Jasmine is a behavior-driven development framework for testing JavaScript code
and Karma is a fantastic test-runner that will watch your JavaScript files for any changes and run the tests automatically, much like NCrunch does for .NET unit tests.
This time we'll install all the necessary packages from the npm
repository so that we can benefit from VS2015's great npm
built-in support.
npm install jasmine-core --save-dev
npm install karma karma-jspm karma-babel-preprocessor@5.2.2 karma-chrome-launcher karma-jasmine karma-coverage --save-dev
Notice the --save-dev
flag at the end of both commands. This tells npm that we'll only need these packages at development time.
Version mismatch
You might have noticed that we are installing the karma-babel-preprocessor
package using a specific version.
That's because, at the time of this writing, Babel 6 is already out, but the current jspm
(0.16.19) and SystemJs
(0.19.9) versions don't support it yet. The latest version of karma-babel-preprocessor
is already based on Babel 6. Version 5.2.2 is the latest version currently supported by jspm
.
Babel 6 support is planned for SystemJS 0.20.0, which should be released soon.
We still need to configure Karma, but let's write a unit-test in Jasmine before we go any further.
fullName.spec.js
Inside the test
folder, create the file that will hold our test. I called it fullName.spec.js
, but it really doesn't matter.
The test itself will be as simple as our viewmodel, of course, but remember: the idea here is to get Jasmine and Karma up and running, not to explore the deep intricacies of JavaScript unit testing. My test file looks as follows:
import {FullName} from "../../wwwroot/src/viewmodels/fullName";
describe("FullName module", () => {
let sut; // System Under Test
beforeEach(() => {
sut = new FullName();
});
it("should exist", () => {
expect(sut).toBeDefined();
});
it("should concatenate name and surmane", () => {
// we're dealing with Knockout observables, remember!
sut.name("Sergi");
sut.surname("Papaseit");
// I want this test to fail, initially.
// You'll see why
expect(sut.fullName()).toEqual("");
});
});
Configuring Karma
Karma requires a quite a bit of configuration. Thankfully, a big chunk of the work will be done for us just by running a command and answering a couple of questions.
Again, then, within a Command Prompt, go to the root of the project (one level above the wwwroot
folder), and run:
karma init
As I said, this will present you with a set of questions. Here's how to answer (by basically accepting every default answer):
Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine
Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no
Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
>
What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
>
Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>
Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes
This will create a karma.conf.js
file in the folder where you run the command. Now we'll have to edit this config file to tell Karma we're using a transpiler (BabelJs).
jspm
Open karma.conf.js
, locate the line where the framweorks
are defined and add jspm
to the array.
frameworks: ["jspm", "jasmine"],
Next, we weed to configure jspm
. Add the following elements anywhere in the config file (I added it just below frameworks
):
jspm: {
// Edit this to your needs
loadFiles: ["wwwroot/src/**/*.js", "test/**/*.js"],
paths: {
"*": "*.js"
},
packages: "wwwroot/jspm_packages"
},
proxies: {
'/base/jspm_packages/': "/base/wwwroot/jspm_packages/"
},
The "jspm" section basically tells Karma where the jspm
packages and our own javascript files are; the "proxies" section maps the packages path so that Karma can understand where wwwroot
stands in relation to it's baseUrl. Remember that we removed the baseUrl
setting from config.js
? That would confuse Karma if left there. Adding the proxies completes the mapping.
Needles to say, adapt the loadFiles
section to your needs in case you haven't followed my same folder structure.
Babel preprocessor setup
Ok people, hold on tight because this is the last step! If we tried to run the tests with Karma as it stands, Karma wouldn't be able to interpret the ES2015 code. Much like the application itself, Karma needs a transpiler.
Place the following somewhere in karma.config.js
:
preprocessors: {
"test/**/*.js": ["babel"],
"wwwroot/src/**/*.js": ["babel"]
},
babelPreprocessor: {
options: {
sourceMap: "inline",
modules: "system",
moduleIds: false,
optional: [ ]
}
},
In the preprocessors
section we're just telling Karma to use BabelJs to pre-process both our sources and the unit tests. In the babelPreprocessor
section we can configure Babel itself. We could, for instance, use optional ES2016 features like class decorators.
Time to se what the fuss is all about
If you haven't used Karma or any other JavaScript test runner before you might be wondering why on earth you'd want to go through all this trouble to set it up. Then again, if you're using NCrunch with Visual Studio you probably already know what to expect and know that it's brilliant.
Once more, open a Command Prompt and go to the root of the project, the folder where karma.conf.js
lies and run the following command:
karma start
Karma will open a broswer window (a Chrome instance), but you can minify and ignore it for now. It becomes useful in case you want to debug some failing tests.
Other than dat, on the console you will see that Karma has run 2 tests, and that one of them is failing. I did that on purpose, remember?
11 01 2016 16:42:01.451:WARN [karma]: No captured browser, open http://localhost:9876/
11 01 2016 16:42:01.469:INFO [karma]: Karma v0.13.19 server started at http://localhost:9876/
11 01 2016 16:42:01.477:INFO [launcher]: Starting browser Chrome
11 01 2016 16:42:03.329:INFO [Chrome 47.0.2526 (Windows 10 0.0.0)]: Connected on socket /#8Qq-vrPlgUkOls7iAAAA with id 26439104
Chrome 47.0.2526 (Windows 10 0.0.0) FullName module should concatenate name and surmane FAILED
Expected 'Sergi Papaseit' to equal ''.
at Object.<anonymous> (C:/Software Development/Projects/KPIT-Blog-MVC/src/Web/test/fullName.spec.js:26:44)
Chrome 47.0.2526 (Windows 10 0.0.0): Executed 2 of 2 (1 FAILED) (0.032 secs / 0.006 secs)
Time to make that test pass; prepare to be amazed!
Karma magic
Open fullName.spec.js
and change this line expect(sut.fullName()).toEqual("");
into:
expect(sut.fullName()).toEqual("Sergi Papaseit");
And save the file while keeping an eye on the console... BAM! Karma has detected changes in one of the files it's been watching and has automatically run the tests again.
That means not having to take any extra steps to run your tests while you develop. Karma for the front end and NCrunch for the back-end and you'll know immediately if your code breaks any of the existing tests. If that doesn't put a smile in your face I don't know what will.
Can I use this in the wild?
You definitely can, but what I've shown here is not how we use it in a production environment. The setup of this blog actually requires SystemJs to transpile with Babel on the fly, in the browser. As you can imagine, this imposes quite a speed penalty on the application.
So how doe we use it? Without going into too much detail, because that is a whole post in and of itself, here's our setup:
- The
src
folder containing our JavaScript source files (models, viewmodels, etc) lives outsidewwwroot
- We have
gulp
tasks defined that transpile the *.js files upon building the solution. - Other gulp tasks copy the transpiled files into a
dist
folder insidewwwroot
. - SystemJs looks inside the
dist
folder for source files, which have already been transpiled. - We have a "watch" gulp task that keeps an eye on any of the source files for changes. If they change, they are rebuilt and placed inside the
dist
folder. - Karma still uses the files inside
src
to run the tests. It's ok if those are transpiled on the fly.
And there you have it. If you have any questions, remarks or comments, feel free to let me know!