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...

  1. File / New Project
  2. Select the .NET Framweork 4.6.1 and ASP.NET Web Application
  3. From the ASP.NET 5 Templates, select Web Application
  4. Inside wwwroot, create an src folder. This folder will hold our JavaScript sources.
  5. Inside src, create a viewmodels folder.
  6. Inside src, create a pages folder.
  7. One level up from wwwroot, create a test 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:

  1. Import the Main module.
  2. Import the KnockoutJs viewmodel for the current view, along with any other necessary modules (like, say, utility classes).
  3. 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

  1. We won't need to test that file, and
  2. 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:

  1. The src folder containing our JavaScript source files (models, viewmodels, etc) lives outside wwwroot
  2. We have gulp tasks defined that transpile the *.js files upon building the solution.
  3. Other gulp tasks copy the transpiled files into a dist folder inside wwwroot.
  4. SystemJs looks inside the dist folder for source files, which have already been transpiled.
  5. 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.
  6. 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!