Visual Studio Javascript AMD Unit Testing

Unit Testing AMD-Style Javascript In Visual Studio

I’m currently working on a new MVC 5 client/server project. This project is an SPA (single-page application), which means the amount of Javascript client code easily dominates the amount of C# server code. In order to keep all that Javascript manageable, I’ve been writing AMD-style (asynchronous module definition) Javascript for use with Require.js. Unfortunately, unit testing AMD-style Javascript in Visual Studio doesn’t get nearly as much attention as does unit testing C# code. But the growing body of javascript code with absolutely zero test coverage was beginning to concern me. I decided it was time to address the situation.

My Requirements

  1. Must integrate with Visual Studio test framework so client and server tests are in one place
  2. Must be designed to work with AMD-style Javascript
  3. Must support easy overriding of dependencies so I can mock/stub/fake etc.
  4. Should run without launching a browser so that tests finish quickly
  5. Should be free (preferably open source)

I scoured Google looking for options and found only two:

Resharper Probably a good tool, but it’s costly and does way more than I need.
Chutzpah Looks promising at first… but uses PhantomJS, which conflicts with my video card.

Then I took a closer look at Google’s V8 Javascript Engine. The LESS processor was already depending on it so maybe I could too.

How to use V8 Javascript Engine for AMD Unit Testing

1. Create a test project and add V8 Javascript Engine

Technically, we’re going to use ClearScript.V8, which is actually a wrapper written by Microsoft that does all the heavy lifting for us.

Search for ClearScript.V8 in the NuGet package manager or type the following in the NuGet console:

PM> Install-Package ClearScript.V8

2. Create a test class

[TestClass]
public class UnitTest1
{
    V8ScriptEngine v8;

    [TestInitialize]
    public void Setup()
    {
        v8 = new V8ScriptEngine();
        v8.AddHostType("Assert", typeof(Assert));
        v8.AddHostType("Console", typeof(Console));
        v8.Execute(@"
            var console = {
                log: function(obj) {
                    if (typeof obj === 'object') {
                        obj = JSON.stringify(obj);
                    }
                    else if (typeof obj === 'function') {
                        obj = 'function';
                    }
                    Console.WriteLine(obj);
                }
            };
        ");
    }

    [TestCleanup]
    public void Teardown()
    {
        v8.Dispose();
        v8 = null;
    }
		
    [TestMethod]
    public void TestMethod()
    {
        v8.Execute("console.log('hello world');");
    }
}

Run the test and check the output. No surprise, you’ll see ‘hello world’. It’s really that simple to run Javascript from C#.

Hello World unit test passed

So this already meets three of my requirements so far. Namely, it works via the Visual Studio test framework, it didn’t need to launch a browser, and it’s free. Nice. Time to do something more useful.

3. Create a simple class to mimic Require.js

using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.ClearScript.V8;
using Microsoft.VisualStudio.TestTools.UnitTesting;

public class RequireJS
{
    V8ScriptEngine v8;
    Dictionary<string, object> definitions;

    public RequireJS(V8ScriptEngine v8)
    {
        this.v8 = v8;
        definitions = new Dictionary<string, object>();
    }

    public void Register(String moduleName, String path)
    {
        var scriptFileInfo = new FileInfo(path);
        Console.WriteLine("Sourcing " + moduleName + " from " + scriptFileInfo.FullName);

        using (var scriptReader = new StreamReader(scriptFileInfo.OpenRead()))
        {
            var script = scriptReader.ReadToEnd();
            v8.Execute(script);
        }
    }

    public void Define(String moduleName, object obj)
    {
        definitions[moduleName] = obj;
    }
    
    public object Require(String dependencyName)
    {
        object definition;
        if (!definitions.TryGetValue(dependencyName, out definition))
            Assert.Inconclusive(
                "Dependency " + dependencyName + " hasn't been loaded"
            );
        return definition;
    }
}

4. Expose the RequireJS object to the script engine

[TestClass]
public class UnitTest1
{
    // ...
    RequireJS requireJs;
    
    [TestInitialize]
    public void Setup()
    {
        // ...
        
        requireJs = new RequireJS(v8);
        v8.AddHostObject("fakeRequireJs", requireJs);
        v8.Execute(@"
            var require =
                function(name) {
                    return fakeRequireJs.Require(name);
                };
            var exports = {};
        ");
        v8.Execute(@"
            var define = 
                function(name, deps, body) {
                    // Shift to handle anonymous definitions
                    if (typeof deps === 'function') {
                        body = deps;
                        deps = name;
                        name = '';
                    }

                    // 'resolve' the dependencies in place
                    deps = deps || [];
                    for (var i = 0; i < deps.length; i++)
                        deps[i] = require(deps[i]);
                    
                    fakeRequireJs.Define(name, body.apply(null, deps));
                };
            define['amd'] = true; // Some libraries look for this
        ");
    }

    [TestCleanup]
    public void Teardown()
    {
        // ...
        requireJs = null;
    }

    // ...
}

Consider this fictitious system under test:

define('model/calculator', ['underscore'], function(_) {
    return {
        add: function(items) {
            return _.reduce(items, function(a, b) { return a + b; }, 0);
        }
    };
});

I can now write a unit test that looks like this:

[TestMethod]
public void Add()
{
    requireJs.Register("underscore",
        "../../../Scripts/lib/underscore/underscore-1.6.0.js");

    requireJs.Register("model/calculator",
        "../../../Scripts/app/model/calculator.js");
    
    v8.Execute(@"
        // ARRANGE
        var calculator = require('model/calculator');
        
        // ACT
        var result = calculator.add([1, 2, 3]);
        
        // ASSERT
        Assert.AreEqual(6, result);
    ");
}

5. Some libraries don’t play well… so some fine tuning is required.
For instance, supporting knockout required some trickery including some new ways to define a module: (notice the added resolver parameter)

public class RequireJS
{
    // ...

    public void Register(/* ... */,
        Func<V8ScriptEngine, object> resolver = null)
    {
        // ...

        if (resolver != null)
            definitions[moduleName] = resolver(v8);
    }

    public void DefineFromJavascript(String moduleName, String javascript)
    {
        Console.WriteLine("Sourcing " + moduleName + " from internal javascript");
        definitions[moduleName] = v8.Evaluate(javascript);
    }
}

The Setup method also needs to be updated as follows:

[TestInitialize]
public void Setup()
{
    // ...

    // Knockout requires 'require' and 'exports'.
    // pass in our mock versions of these dependencies
    requireJs.DefineFromJavascript("require", "require");
    requireJs.DefineFromJavascript("exports", "exports");

    // Knockout just dumps its methods on the exports object
    requireJs.Register("knockout",
        "../../../Scripts/lib/knockout/knockout-3.1.0.js",
        _ => _.Evaluate("exports"));

    requireJs.Register("underscore", 
        "../../../Scripts/lib/underscore/underscore-1.6.0.js");
}

6. Refactor to put reusable functionality in a base class

public abstract class V8TestBase
{
    protected V8ScriptEngine v8;
    protected RequireJS requireJs;

    protected abstract void Setup(RequireJS requireJs);

    [TestInitialize]
    public void BaseSetup()
    {
        v8 = new V8ScriptEngine();
        requireJs = new RequireJS(v8);
        // ...
        
        // Allow child classes to do their own setup
        Setup(requireJs);
    }

    [TestCleanup]
    public void Teardown()
    {
        v8.Dispose();
        v8 = null;
        requireJs = null;
    }
}

Don’t forget to make your test class derive from your new base class:

[TestClass]
public class UnitTest1 : V8TestBase
{
    protected override void Setup(RequireJS requireJs)
    {
        // Register libraries specific to this test class here
    }

    // ...
}

7. Improve the output from failed test cases
As it stands now, assertion failures result in very long, very ugly nested exception messages. But we can wrap each test case in a way that can dig into an exception and pull out the pertinent details.

Comparison of assertion failures between naked script executions and wrapped calls

Comparison of assertion failures between naked script executions and wrapped calls

Create a function to wrap calls into the V8 engine. When you find an exception, attempt to unwrap it to find the inner UnitTestAssertException.

public abstract class V8TestBase
{
    // ...
    
    protected void Test(String javascript)
    {
        var wrappedJs = "(function(){" + javascript + "})();";
        try
        {
            v8.Execute(wrappedJs);
        }
        catch (Exception e)
        {
            UnitTestAssertException ae;
            throw TryGetAssertException(e, out ae) ? ae : e;
        }
    }

    private bool TryGetAssertException(Exception e, out UnitTestAssertException ae)
    {
        ae = null;
        if (e == null)
            return false;

        ae = e as UnitTestAssertException;
        if (ae != null)
            return true;

        return TryGetAssertException(e.InnerException, out ae);
    }
}

8. Introduce a view model to test

define('viewmodel/shoppingCart', ['knockout', 'underscore'], function(ko, _) {
  function ShoppingCart()  {
    var self = this;
    
    self.salesTax = ko.observable(0);

    self.items = ko.observableArray([]);

    self.total = ko.computed(function() {
      var salesTax = self.salesTax();
      var total =
        _.chain(self.items())
          .map(function(item) {
            var taxOnThisItem =
              item.category === 'Baby Food' ? 0 : salesTax;
            return item.price * (1 + taxOnThisItem);
          })
          .reduce(function(a, b) { return a + b; }, 0)
          .value();
      return Math.round(total * 100) / 100;
    });
  }

  ShoppingCart.prototype.add = function(item) {
    this.items.push(item);
  };
  
  return new ShoppingCart();
});

And what the unit tests would look like:

[TestClass]
public class ShoppingCartTests : V8TestBase
{
    protected override void Setup(RequireJS requireJs)
    {
        requireJs.Register("viewmodel/shoppingCart",
            "../../../Scripts/app/viewmodel/shoppingCart.js");
    }
    
    [TestMethod]
    public void NoSalesTaxOnBabyFood()
    {
        Test(@"
            // ARRANGE
            var shoppingCart = require('viewmodel/shoppingCart');
            shoppingCart.salesTax(0.13);
            
            // ACT
            shoppingCart.add({
                name: 'Banana Puree',
                category: 'Baby Food',
                price: 1.00
            });
            
            // ASSERT
            Assert.AreEqual(1.00, shoppingCart.total());
        ");
    }
    
    [TestMethod]
    public void SalesTaxOnBathAndBeauty()
    {
        Test(@"
            // ARRANGE
            var shoppingCart = require('viewmodel/shoppingCart');
            shoppingCart.salesTax(0.13);
            
            // ACT
            shoppingCart.add({
                name: 'Shampoo',
                category: 'Bath & Beauty',
                price: 10.00
            });
            
            // ASSERT
            Assert.AreEqual(11.30, shoppingCart.total());
        ");
    }
}

Enhancements

Here are some worthwhile ideas for enhancements that I’ll leave as an exercise:

  • modify Register to accept a filename with a version placeholder so that you can upgrade your Javascript libraries without breaking your tests
  • modify exception handling to report the location of errors in your Javascript
  • incorporate your favourite stubbing/mocking/faking library

Caveats

I’ve described a method for unit testing AMD-style Javascript within Microsoft’s Visual Studio test framework that meets all my initial requirements. It’s working well so far. There are however some weaknesses.

  • First of all, there is no notion of measuring code coverage. You’re entirely on your own here.
  • There is some trial and error involved when adding a new Javascript library (e.g. Of the three libraries I’ve registered so far, two required special consideration)
  • There is no Intellisense support while writing Javascript as C# strings

I hope you found this useful. Good luck with your AMD javascript unit testing!
Visual Studio Javascript AMD Unit Testing

One thought on “Unit Testing AMD-Style Javascript In Visual Studio”

Comments are closed.