Checkout Radzen Demos and download latest Radzen!
Unit tests are a first class citizen in Angular 2. The officially recommended test runner is Karma and the testing library is Jasmine. Karma executes unit tests in a browser (sometimes a headless one such as PhantomJS). Thus unit tests run in a close to real-world environment.
In this blog post I will show you a different approach to running tests - with Mocha, Chai and Sinon. Mocha has a test runner which works with Node and does not need a browser. We need Chai and sinon because Mocha does not include assertion and mocking libraries. We will use Webpack for bundling and module loading.
Setup
We need a simple Angular 2 Webpack application. Let’s use the one from the official Webpack introduction.
Then we need to install some npm packages:
- mocha (test runner)
- mocha-webpack (build the tests with Webpack before running them)
- chai (assertion library)
- @types/chai (TypeScript definitions for chai)
- sinon (mocking library)
- @types/sinon (TypeScript definitions for sinon)
- webpack-node-externals (exclude server-side node modules from the build)
- jsdom (provides stubs for DOM API required by Angular - HTMLElement, XMLHttpRequest)
npm install --save-dev mocha mocha-webpack chai @types/chai sinon @types/sinon webpack-node-externals jsdom
Add some tests
Modify the AppComponent from the Webpack introduction tutorial to implement a few simple tasks.
src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: require('./app.component.html')
})
export class AppComponent {
value = 0;
onIncrementClick() {
this.value = Math.min(100, ++this.value);
}
onDecrementClick() {
this.value = Math.max(-100, --this.value);
}
}
src/app/app.component.html
<main>
<h2>Value: {{ value }}</h2>
<button class="increment" (click)="onIncrementClick()">+</button>
<button class="decrement" (click)="onDecrementClick()">-</button>
</main>
Add tests for those tasks.
src/app/app.component.spec.ts
import { getTestBed, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { By } from '@angular/platform-browser';
import { expect } from 'chai';
import { spy } from 'sinon';
describe(`AppComponent ${i}`, () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [AppComponent]
});
});
afterEach(() => {
getTestBed().resetTestingModule();
});
it('should display 0 as initial value', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const h2 = fixture.debugElement.query(By.css('h2'));
expect(h2.nativeElement.textContent).to.equal('Value: 0');
});
it('should increment the value', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.componentInstance.onIncrementClick();
fixture.detectChanges();
const h2 = fixture.debugElement.query(By.css('h2'));
expect(h2.nativeElement.textContent).to.equal('Value: 1');
});
it('should invoke onIncrementClick when the user clicks the increment button', () => {
const fixture = TestBed.createComponent(AppComponent);
const onIncrementClick = spy(fixture.componentInstance, 'onIncrementClick');
const button = fixture.debugElement.query(By.css('.increment'));
button.triggerEventHandler('click', {});
expect(onIncrementClick.called).to.equal(true);
});
/* snip */
});
There are a few differences with the Karma/Jasmine approach.
- We reset the testing module after each test because Zone.js does not support Mocha yet.
- Assertions are done with Chai:
expect(value).to.equal(true)
instead ofexpect(value).toEqual(true)
. - Mocking is done with Sinon.
Run the tests
First we need to create e Webpack config file that will ignore Node server-side modules during packaging.
config/webpack.mocha.js
var webpackMerge = require('webpack-merge');
var nodeExternals = require('webpack-node-externals');
var commonConfig = require('./webpack.test');
module.exports = webpackMerge(commonConfig, {
externals: [
nodeExternals()
]
});
Then we need to create a shim file which will load all dependencies and configure Angular for testing. This
file is very similar to karma-test-shim.js
from the official Webpack introduction tutorial.
config/mocha-test-shim.js
var jsdom = require('jsdom')
var document = jsdom.jsdom('<!doctype html><html><body></body></html>');
var window = document.defaultView;
global.document = document;
global.HTMLElement = window.HTMLElement;
global.XMLHttpRequest = window.XMLHttpRequest;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(browser.BrowserDynamicTestingModule, browser.platformBrowserDynamicTesting());
The biggest difference is that we are using jsdom
to provide stubs for a few DOM APIs that Angular 2 needs.
We also need a configuration file for mocha-webpack
in order to avoid passing all options through command line.
mocha-webpack.opts
--webpack-config config/webpack.mocha.js
--require config/mocha-test-shim.js
src/**/*.spec.ts
The first line specifies the Webpack config file, the second line includes the shim and the last line specifies where the test files are.
Finally we can update the npm test scripts in package.json
to run our tests with mocha-webpack
.
"scripts": {
"test": "mocha-webpack",
"test:watch": "mocha-webpack --watch"
}
Running the tests is as simple as npm test
. To run them in watch mode use npm run test:watch
.
Compared to Karma
Let’s compare the test running time of Karma and Mocha. The angular2-webpack-mocha github repo is a sample application configured for both Karma and Mocha. Here are the results of running the tests 10 times:
Mocha is more than one second faster than Karma. Running the tests 100 times show 15-20% improvement over Karma.
Testing in watch mode also feels snappier with Mocha.