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

@nip3o

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?

Made with Slides.com