Learn How to Write Angular 2 Style Unit Tests with Angular 1.X Code

Written by Simon Ramsay


The development of Angular 2 is on its way with much talk, hype, and fanfare. Surprisingly, there is not much being said about unit testing. As a responsible developer, you have undoubtedly already written a giant unit test suite to achieve ‘near perfect’ code coverage, and I'm here to let you know, there is no need to throw that all away! While Angular 2 is currently in ‘developer preview’, implying that nothing serious should be built with it currently, the Angular Team has already created a giant wealth of unit tests and a custom testing setup: (https://github.com/angular/angular/tree/master/modules/angular2/test).
Unfortunately, it is not easy to set this up for yourself. So this post should help you get started.

Custom Angular 2 Training

Getting Started

So, SuperService was an Angular 1 service that has been prepped for Angular 2 along with its unit test.

class SuperService {  
  static $inject = ['$log'];

  constructor(
    private $log
  ) {

  }

  doSomething () {
    this.$log('we did something');
  }
}

export {SuperService};  
angular.module('someApp').service('SuperService', SuperService);  
//SuperService.spec.ts
import {SuperService} from 'somewhere/SuperService';  
import {expect} from 'chai';

export function main() {  
  describe('SuperService', () => {
    it('should do something with the provided $log mock', () => {
      let _msgs = []; 
      let logMock = (msg) => {
        _msgs.push(msg);
      };

      expect(_msgs).to.have.length(0);     
      let superService = new SuperService(logMock);
      superService.doSomething();

      expect(_msgs).to.have.length(1);
    });
  });
}

What happens when you try to run transitional unit tests as if they were Angular 1? Karma outputs a host of confusing error, such as:

  • Uncaught ReferenceError: define is not defined
  • Executed 0 of 0 ERROR (0.003 secs / 0 secs)

So how did the Angular Team get this to work?

First, a custom test runner is needed to make this all work. It does a lot of things, but in short:

  • sets up module paths so that the modules can be loaded
  • stops Karma from exiting before the async modules are loaded
  • loads the test modules using system.js
  • manually runs each test module
__karma__.loaded = function() {}; //Tell karma to start when we are good and ready;; thanks angular 2

System.baseURL = '/base/client/dist/'; //where we keep our test files  
System.paths = {  
  '*': '/base/client/dist/*.js',
  'chai' : '/base/node_modules/chai/chai.js'
};

function onlyUnitTestFiles(path) {  
  return /-spec\.js$/.test(path);
}

function karmaFileToModule(fileName) {  
  return fileName.replace(System.baseURL,'')
                  .replace('.js','');               
}

 Promise.all(
  Object.keys(window.__karma__.files) // All files served by Karma.
  .filter(onlyUnitTestFiles)  //only directly load the test modules
  .map(karmaFileToModule) //map filename to module name
  .map(function(modName){ //load each test module
    return System.import(modName)
      .then(function(mod) {
         if (mod.hasOwnProperty('main')) {
            mod.main(); //expose the tests
          } else {
            throw new Error('Test module: ' + modName + ' does not implement main() method.');
          }
      })
      .then(function() {
        __karma__.start(); //run the tests
      })
      .then(null, function(error) {console.error('Failed to load:', error);});
  }));

Then, before each test run, transpile typescript down to es5 so it can be run in a browser.

//run your build tasks
//then run your tests

Finally, start up karma but with some key quirks. It is important to serve but not include the transpiled files because they are not intended to be manually included on the page but loaded asynchronously.

//karma.conf.js
module.exports = function(config) {  
  config.set({
    files : [
      'es6-module-loader.js',
      'system.js',
      ...
      'test-runner.js',
      { 
        pattern : '{path to transpiled files}/**/*.js',
        included : false,
        serve : true 
      }
    ],
    frameworks: ['mocha', 'chai'],
    browsers: ['Chrome'],
    ...
  });
};

By following these steps, you'll find that your tests will now run properly. We wish you luck! Want to read more about writing your Angular 1.x applications in an Angular 2.0 style? Read Yuri Takhteyev's (Our CTO) blog here, and check out our Angular course here.

View and download Rangle’s Angular 2 Training Book.

Our extensive Angular 2 course book, created in-house by Rangle’s team of JavaScript experts, covers fundamental topics including how to get started with the Angular 2 toolchain.