Angular, Typescript, RequireJS, and More, Pt. 3

This is part 3 of our series. You can find part 1 here and part 2 here.

This post’s topic is unit testing. We’ll be using Mocha, Chai, and Sinon.JS. Everything will be run with Karma on PhantomJS, so we will be able to run this easily on a CI server such as Travis or Jenkins.

Step 1: Set up Karma

Karma is pretty straight-forward, there’s a short wizard to step through.

$ npm install -g karma-cli
$ karma init

Be sure to select mocha, yes, PhantomJS, and enter a glob pattern matching your build directory. I strongly recommend having your test files live next to the files that they are testing, for wonky-typescript-compilation reasons explained below. If you’re using our gulpfile from part 1, this should be set to src/js/**/*.js, as karma is run from the root of our project directory. Let karma set you up with a default RequireJS configuration file. We’ll have to tweak it, but it’s easier to tweak when you’re starting from somewhere.

Once you have a karma.conf.js file and a main-test.js file, we’ll have to install the plugins required to get everything going.

$ npm install --save-dev karma-mocha karma-chai karma-sinon karma-phantomjs-launcher karma-requirejs karma-chai-sinon

If you have NPM v3 or higher installed (which I do recommend), you may have to install the peerDependencies manually:

$ npm install --save-dev mocha chai sinon sinon-chai

We do have to go in and modify the generated files. First, we have to add some frameworks to our array in karma.conf.js

module.exports = function(config) {
  config.set({
    //...
    frameworks = [
      'mocha',
      'requirejs',
      'chai',
      'sinon',
      'chai-sinon'
    ],
    //...

Then, we have to modify our main-test.js so RequireJS knows where to find our application dependencies.

require.config({
//...
  paths: {
    "angular": "./dist/bower_components/angular/angular",
    "angular-mocks": "./dist/bower_components/angular-mocks/angular-mocks",
    "angular.ui.router": "./dist/bower_components/angular-ui-router/release/angular-ui-router",
    "angular.ui.bootstrap": "./dist/bower_components/angular-bootstrap/ui-bootstrap",
    "chai": "./node_modules/chai/chai",
    "sinon": "./node_modules/sinon/pkg/sinon"
  },

  shim: {
    "angular": {
      "exports": "angular"
    },
    "angular-mocks": ["angular"],
    "angular.ui.router": ["angular"],
    "angular.ui.bootstrap": ["angular"]
  },
//...
});

Finally, we’ll have to install the Angular mock library, as well as its typings, and typings Mocha, Chai, and Sinon.

$ bower install angular-mocks
$ tsd install mocha chai sinon sinon-chai --save

Create Your First Test

As I said above, our tests will live next to our regular files. This makes the compilation logic much easier. We’ll follow a particular pattern, so that it would be easy to filter out our spec files when bundling. All files will have a .spec.ts extension. We’re going to get right into the complicated stuff, so follow along below.

Given the following service:

import angular = require("angular");
import {app} from "../app.module";

export interface ITestService {
    getPost(id: number): angular.IHttpPromise<IJsonPlaceholderPost>;
}

export interface IJsonPlaceholderPost {
  userId: number;
  title: string;
  body: string;
}

export class TestService {

  static $inject = ["$http"];

  public message: string;

  private $http: angular.IHttpService;

  constructor($http: angular.IHttpService) {
    this.$http = $http;
  }

  public getPost(id: number): angular.IHttpPromise<IJsonPlaceholderPost> {
      return this.$http.get(`http://jsonplaceholder.typicode.com/posts/${id}`);
  }

}


app.service("TestService", TestService);

We can test it like so:

/// <amd-dependency path="angular-mocks" />
/// <amd-dependency path="./testService" />
import {expect} from "chai";
import {spy} from "sinon";
import {mock, IHttpBackendService, IScope, auto} from "angular";
import {TestService} from "./testService";

// Describe blocks are the starting point of mocha testing
describe("TestService", () => {
  // Have a reference to save our service for testing later.
  var service: TestService;
  
  // This runs before each child block of our current block.
  beforeEach(() => {
    // tell angular to mock our entire application
    mock.module("app");
    // and retrive the injector to retrive anything we need.
    mock.inject(($injector: auto.IInjectorService) => {
      service = $injector.get<TestService>("TestService");
    });
  });


  it("should be a TestService", () => {
    // Always good to make sure we're testing the right class.
    expect(service).to.be.an.instanceOf(TestService);
  });

  // complications below!
  // Our beforeEach block above runs once, then runs this entire block
  describe("#getPost", () => {
    // saving for later
    var $httpBackend: IHttpBackendService;
    var $rootScope: IScope;

    beforeEach(() => {
      mock.inject(($injector: auto.IInjectorService) => {
        // We need to $apply things for promises to resolve in angular
        $rootScope = $injector.get<IScope>("$rootScope");
        // and set up a mock response for hitting our mocked backend
        $httpBackend = $injector.get<IHttpBackendService>("$httpBackend");
        $httpBackend.expect(
          "GET",
          // with regex
          /http:\/\/jsonplaceholder.typicode.com\/posts\/(\d+)/,
          undefined,
          undefined
        )
        .respond((method: string, url: string, data: any, headers: any) => {
          console.log(url);
          return "Hello";
        });
      });
    });

    it("should call jsonplaceholder", () => {
      var randomPost = Math.floor(Math.random() * Number.MAX_VALUE);
      // Because we can't used chai-as-promised, spying is a good way
      // to see the result of a promise
      var httpSpy = spy();
      service.getPost(randomPost).then(httpSpy);
      // Flush the requests out
      $httpBackend.flush();
      // Apply to resolve promises
      $rootScope.$apply();
      // Check any expectations.
      expect(httpSpy).to.be.called;
      // We can test what the promise was called with by using the
      // calledWith assertion as well.
    });
  });
});

The above is a good basis to get started. It shows you the angular-specific things that you need to get started, as well as some of the gaps that can confuse beginners, such as the fact that promises are tied to Angular’s digest cycle, and so an $apply call must be made before they will resolve.

To test a controller, we can request the $controller service from the injector, and request our controller by name. Because of this, if your application is entirely made up of directives, it is still a smart idea to register any controllers with an app.controller call.

Running our Tests

First, to ensure everything works now, run karma start in your terminal, and squash any errors you see. Some things to look out for:

  1. “No timestamp for $SCRIPT”: This means that, for whatever reason, Karma hasn’t loaded a particular file into the test context. Check your files or frameworks key in your karma.conf.js to make sure everything is getting loaded.

  2. “Error: No provider for “framework:$FRAMEWORK”!: You have a framework listed that isn’t installed. NPM that up!

  3. Module timeouts: Something somewhere in your code is probably creating a circular dependency, which RequireJS cannot handle. Find that and fix it! If your application is made up of several Angular modules, a good recommendation is to not require anything from a different module, and instead require things in your entry-point for that module.

To add our tests to our gulpfile, we don’t need any fancy plugins.

var gulp = require('gulp');
var Server = require('karma').Server;

gulp.task('test', function (done) {
  new Server({
    configFile: __dirname + '/karma.conf.js',
    singleRun: true
  }, done).start();
});

gulp.task('test:watch', function (done) {
  new Server({
    configFile: __dirname + '/karma.conf.js'
  }, done);
});

Now, you can just have your CI server run gulp test to test our application.

That is that! This is the end of the series (finally). I hope you’ve managed to get your application up and running, or maybe you’ve decided to check out Angular 2 instead. I know that’s what I’d be doing now.

I feedback. Let me know what you think of this article on Twitter .