Unit-testing in Javascript
Niclas
- Not an expert on testing.
- Did my Master Thesis on testing web-based applications and working with TDD.
- RSpec, Selenium, Jasmine (Knockout/jQuery)
- Working with a quite test-heavy web-application.
- py.test, Selenium, Jasmine (AngularJS)
niclas.olofsson@cambio.se
Outline
Some of the tools we use for the client-side testing.
Basic structure and workflow
Testing different Angular components
Tips and tricks
Replacing implementations
Frameworks
Many tools, hard to know which tool does what
Where to debug?
Why does X behave like this?
A Javascript-based test runner.
- Finds application files and tests.
- Starts a server which serves application code.
- Watches files for changes and push them out to all clients.
- A client can be any Javascript-enabled browser.



A Javascript-based testing framework.
- Defines a way of writing test specifications.
- Provides methods for assertions.
- Provides helpers for stubbing and mocking.
ngMock
An Angular module providing Angular-specific test helpers.
- Global helpers for dependency injection and modules.
- Mock implemenations of $httpBackend, $interval, $timeout and a few other Angular components.
Basic workflow
What needs to be done to write and run a test?
- Start the Karma server.
- Open a browser.
- Write a test.
- Tests are run continuously and the result is displayed in the terminal.
Traditional Karma-workflow
Running the test suite
With Maven - tests are run when building
> cd nova-client-html
> mvn
(...)
[INFO] Executing external process:
[INFO] node node_modules/karma/bin/karma start karma.conf.js --no-auto-watch --single-run --no-color
[INFO] | INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
[INFO] | INFO [launcher]: Starting browser PhantomJS
(...)
[INFO] | PhantomJS 1.9.8 (Windows 7): Executed 39 of 39 SUCCESS (0.358 secs / 0.538 secs)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 36.645s
Running the test suite
Using Karma directly - temporary solution
> cd nova-client-html
> mvn webtools:devserver
(...)
[INFO] Server started.
> cd nova-client-html/physician-tablet
> karma start --browsers Chrome
INFO [karma]: Karma v0.12.31 server started at http://localhost:9876/
(...)
Chrome 40.0.2214 (Windows 7): Executed 39 of 39 SUCCESS (0.589 secs / 0.585 secs)

(beware of OS power-saving issues)
A basic Jasmine test
function myAddFunction (x, y) {
return x + y;
}
src/main/app/js/adder.js
src/test/app/js/adderTest.js
describe('myAddFunction', function () {
it('returns the sum of the two parameters', function() {
expect(myAddFunction(3, 4)).toEqual(7)
});
it('handles negative numbers correctly', function() {
expect(myAddFunction(3, -4)).toEqual(-1)
});
});
describe() - define a new describing block
it() - define a new test
expect() - make an assertion
toEqual() - matcher to test equality
Once again, with modules
angular.module('NovaApp.services')
.factory('MathService', function() {
return {
myAddFunction: function (x, y) {
return x + y;
}
}
}
src/main/app/js/MathService.js
src/test/app/js/MathServiceTest.js
describe('MathService', function() {
describe('myAddFunction', function () {
it('returns the sum of the two parameters', inject(function(MathService) {
expect(MathService.myAddFunction(3, 4)).toEqual(7)
}));
it('handles negative numbers correctly', inject(function(MathService) {
expect(MathService.myAddFunction(3, -4)).toEqual(-1)
}));
});
});
describe('MathService', function() {
describe('myAddFunction', function () {
var MathService;
beforeEach(inject(function(_MathService_) {
MathService = _MathService_;
}));
it('returns the sum of the two parameters', function() {
expect(MathService.myAddFunction(3, 4)).toEqual(7)
});
it('handles negative numbers correctly', function() {
expect(MathService.myAddFunction(3, -4)).toEqual(-1)
});
});
});
Using beforeEach
It is not possible to use inject() on the function passed to describe().
Called once before each test in the describe
describe() - define a new describing block
it() - define a new test
expect() - make an assertion
beforeEach() - run something before each test in in the current describing block
inject() - inject a dependency into a function
toEqual() - matcher to test equality
Testing Angular components
AngularJS introduces many concepts. How to test different component types?
Filters
angular.module('NovaCommon.filters')
.filter('join', function () {
return function (inputArray, delimiter) {
return (inputArray || []).join(delimiter || ', ');
};
})
describe('NovaCommon.filters.general', function () {
describe('join', function () {
var joinFilter;
beforeEach(inject(function(_joinFilter_) {
joinFilter = _joinFilter_;
}));
it('returns a string of all elements separated by a delimiter', function () {
expect(joinFilter(['Foo', 'Bar', 'Baz'], '.')).toEqual('Foo.Bar.Baz');
});
it('returns empty string if input is null', function () {
expect(joinFilter(null)).toEqual('');
});
});
});
Injectable as filternameFilter.
Services
describe('JournalPaginationService', function() {
var journalPaginationService;
beforeEach(inject(function(_journalPaginationService_) {
journalPaginationService = _journalPaginationService_;
}));
describe('getIdsForBatch', function() {
var ids = [1, 2, 3, 4, 5];
var size = 3;
it('returns the first three elements if currentIndex is 0', function() {
var result = journalPaginationService.getIdsForBatch(ids, size, 0);
expect(result).toEqual([1, 2, 3]);
});
it('should throw an error if currentIndex is too big', function () {
expect(function () {
journalPaginationService.getIdsForBatch(ids, size, 5);
}).toThrow();
});
});
});
Just the same as before.
Controllers
describe('PasswordController', function() {
describe('scope.grade', function() {
var scope, controller;
beforeEach(inject(function($controller) {
scope = {};
controller = $controller('PasswordController', { $scope: scope });
}));
it('sets the strength to "strong" if the password length is >8 chars', function() {
scope.password = 'longerthaneightchars';
scope.grade();
expect(scope.strength).toEqual('strong');
});
it('sets the strength to "weak" if the password length <3 chars', function() {
scope.password = 'a';
scope.grade();
expect(scope.strength).toEqual('weak');
});
});
});
Needs to be instantiated with a scope.
Directives
describe('sumDirective', function() {
var $compile, $rootScope;
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it('fills the element with the appropriate content', function() {
// Compile a piece of HTML containing the directive
var element = $compile('<sum-directive></sum-directive>')($rootScope);
// fire all the watches, so the scope expression {{1 + 1}} will be evaluated
$rootScope.$digest();
// Check that the compiled element contains the templated content
expect(element.html()).toContain('sum of 1 and 1 is 2');
});
});
angular.module('someApp').directive('sumDirective', function () {
return {
restrict: 'E',
template: '<h1>The sum of 1 and 1 is {{1 + 1}}</h1>'
};
});
Faking stuff
Angular uses dependency injection heavily. Almost everything can seamlessly be replaced by an alternative implementation without any magic.
angular.module('NovaApp.services')
.constant('appName', 'My awesome app')
.factory('GreetingService', function(appName) {
return {
greet: function () {
return 'Greetings from ' + appName;
}
}
}
describe('GreetingService', function() {
var GreetingService;
beforeEach(module(function($provide) {
$provide.constant('appName', 'Test');
}));
beforeEach(inject(function(_GreetingService_) {
GreetingService = _GreetingService_;
}));
it('returns a greeting', function() {
expect(GreetingService.greet()).toEqual('Greetings from Test')
});
});
Replacing full implementation
Spies
describe('Spy demo', function() {
var journalSpy, someService;
beforeEach(inject(function(journalFacade, _someService_) {
journalSpy = spyOn(journalFacade, 'findJournalNoteIdsByPatient')
.and.returnValue(['J456', 'J123', 'J321']);
someService = _someService_;
}));
it('fetches sorted journal note IDs', function() {
var result = someService.getSortedJournalNoteIds();
expect(result).toEqual(['J123', 'J321', 'J456']);
});
it('the spy tracks all the arguments of its calls', function() {
someService.getSortedJournalNoteIds();
expect(journalSpy).toHaveBeenCalledWith(123);
});
});
Nova test helpers
nova-client-html/common/src/test/app/common/test-utils
- Stuff needed for bootstrapping the app.
- Defining constants.
- Helpers for creating a NovaModel, creating DTOs
Tips and tricks
Including and excluding tests
describe('including and excluding tests', function () {
xdescribe('none of the tests here will execute', function () {
it('should not execute - spec level', function () {
});
xit('not execute - test level', function () {
});
});
ddescribe('only run these tests', function () {
iit('should execute only this test', function () {
});
it('should execute this one after removing iit', function () {
});
});
});
Since Jasmine 2.1: renamed into fit() and fdescribe()
Debugging

Debuggers and console.log() works as expected.
Thank you!
Any questions?
Unit testing in Javascript
By Niclas Olofsson
Unit testing in Javascript
- 1,162