Unit testing AngularJS

Dr. Gleb Bahmutov PhD
Kensho Boston / NY / SF

Testing?

Testing is boring

Testing is a chore

Testing is a drag

Write 100 tests and earn $0

How to earn money

  • Write working code

  • Iterate

  • Profit

tests?

How to earn money

  • Write working code

  • Iterate

  • Profit

tests

Agile / iteration dev model

Agile / iteration dev model

Write code

Refactor (iterate)

get paid

Agile / iteration dev model

Write code

Refactor (iterate)

get paid

Agile / iteration dev model

Write code

Write tests

Refactor (iterate)

get paid

Keep the app working

Agile / iteration dev model

Write tests

Write code

Refactor (iterate)

get paid

Test Driven Development

Agile / iteration dev model

Write tests

Refactor

Write code

get paid

Agile / iteration dev model

Take away: before we can refactor - write tests

Agile / iteration dev model

but:

writing tests cuts our profits

Agile / iteration dev model

Solution: effective testing

  1. Write fewer tests

  2. Spend less time writing tests

Background: test sizes

Unit tests: small, fast

E2E tests: large, slow

use Protractor (not in this presentation)

use Karma

Sample Jasmine unit test

function add(a, b) { 
    return a + b; 
}
describe('addition', function () {
});
  it('adds numbers', function () {
    expect(add(2, 3)).toEqual(5);
  });
  it('concatenates', function () {
    expect(add('foo', 'bar')).toEqual('foobar');
  });

add.js

add-spec.js

unit test

unit test

Karma configuration

$ npm install -g karma
$ karma init
$ npm install karma-jasmine
module.exports = function(config) {
  config.set({
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],
    files: [
      'add.js', 'add-spec.js'
    ],
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },
    reporters: ['progress'],
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome']
  });
};
$ karma start

Single test run

$ karma start --single-run=true
INFO [karma]: Karma v0.12.21 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 44.0.2403 (Mac OS X 10.10.2)]: 
    Connected on socket 6Hp2TOIipFToLo8rgaIk with id 32903062
Chrome 44.0.2403 (Mac OS X 10.10.2): 
    Executed 2 of 2 SUCCESS (0.007 secs / 0.004 secs)

Karma bonus: coverage

Have we tested everything?

$ npm install karma-coverage --save-dev
// karma.conf.js
module.exports = function(config) {
  config.set({
    ...
    preprocessors: {
      '*.js': 'coverage'
    },
    reporters: ['progress', 'coverage'],
    ...
  });
};

Tip: use coverage preprocessor for code only, not for test files

Karma bonus: coverage

$ karma start --single-run=true
$ open coverage/Chrome/index.html

What source files should we test?

Effective testing

1. Write fewer tests

Test complex large files first

filename     lines   coverage
------------------------------
foo.js        100       25%
bar.js         10       20%

Test complex large files (risk-map)

Test complex large files (risk-map)

Alternate strategy

  • Build feature bundles

  • Coverage by bundle

bundle         unit test coverage
----------------------------------
dist/foo.js          40%
dist/bar.js          70%
dist/baz.js          10%

Bonus

bahmutov/was-tested - code coverage from live websites

Bonus

bahmutov/tested-commits - splits code coverage info from any source by commit

bundle         total coverage = unit tests + live
--------------------------------------------------
dist/foo.js          40%           35%        5%
dist/bar.js          70%           35%       35%
dist/baz.js          10%           10%        0%

We can target unit tests better

Writing fewer unit tests helps, but we still need to spend time writing them

Can we write unit tests in shorter time?

Faster test writing

Injectable

The dependency injection in AngularJS allows you to declaratively describe how your application is wired. This means that your application needs no main() method which is usually an unmaintainable mess. Dependency injection is also a core to AngularJS. This means that any component which does not fit your needs can easily be replaced.

Testable

AngularJS was designed from ground up to be testable. It encourages behavior-view separation, comes pre-bundled with mocks, and takes full advantage of dependency injection. It also comes with end-to-end scenario runner which eliminates test flakiness by understanding the inner workings of AngularJS.

Testability Built-in

=> AngularJS is testable; its testability is directly due to the dependency injection mechanism

angular.module('TodoApp', [])
  .controller('TodoController', function ($scope) {
    $scope.todos = ['do this', 'do that'];
  });

Todo example (controller uses a scope)

Let's test!

angular-mocks - inject modules into tests, sync methods

Angularjs - Unit testing introduction by Nic Kaufman

1, 2, 3, tested ng-describe tutorial

describe('standard unit test', function () {
  beforeEach(angular.mock.module('TodoApp'));
  var $controller, $rootScope;
  beforeEach(angular.mock.inject(
    function (_$controller_, _$rootScope_) {
        $controller = _$controller_;
        $rootScope = _$rootScope_;
    }
  ));
  it('creates a controller', function () {
    var scope = $rootScope.$new();
    $controller('TodoController', {
      $scope: scope
    });
    expect(scope.todos.length).toEqual(2, 'has 2 todo items');
  });
});
angular.module('TodoApp', [])
  .controller('TodoController', function ($scope) {
    $scope.todos = ['do this', 'do that'];
  });

Unit testing Todo example

angular.module('TodoApp', [])
  .controller('TodoController', function ($scope) {
    $scope.todos = ['do this', 'do that'];
  });

Unit testing Todo example

describe('standard unit test', function () {
  beforeEach(angular.mock.module('TodoApp'));
  var $controller, $rootScope;
  beforeEach(inject(function (_$controller_, _$rootScope_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
  }));
  it('creates a controller', function () {
    var scope = $rootScope.$new();
    $controller('TodoController', {
      $scope: scope
    });
    expect(scope.todos.length).toEqual(2, 'has 2 todo items');
  });
});

The same dependency injection that makes AngularJS great fights you during testing

describe('standard unit test', function () {
  beforeEach(angular.mock.module('TodoApp'));
  var $controller, $rootScope;
  beforeEach(inject(function (_$controller_, _$rootScope_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
  }));
  it('creates a controller', function () {
    var scope = $rootScope.$new();
    $controller('TodoController', {
      $scope: scope
    });
    expect(scope.todos.length).toEqual(2, 'has 2 todo items');
  });
});

The same scope magic that makes AngularJS great fights you during testing

describe('standard unit test', function () {
  beforeEach(angular.mock.module('TodoApp'));
  var $controller, $rootScope;
  beforeEach(inject(function (_$controller_, _$rootScope_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
  }));
  it('creates a controller', function () {
    var scope = $rootScope.$new();
    $controller('TodoController', {
      $scope: scope
    });
    expect(scope.todos.length).toEqual(2, 'has 2 todo items');
  });
});

I want

  • to load what I need quickly
  • to avoid the details
ngDescribe({
  module: 'TodoApp',
  controller: 'TodoController',
  tests: function (deps) {
    it('creates a controller', function () {
      expect(deps.TodoController.todos.length).toEqual(2);
    });
  }
});

same unit testing using ng-describe

kensho/ng-describe/test/todos-spec.js

Beautiful 

ngDescribe({
  module: 'TodoApp',
  controller: 'TodoController',
  tests: function (deps) {
    it('creates a controller', function () {
      expect(deps.TodoController.todos.length).toEqual(2);
    });
  }
});

AngularJS (magically) creates controller and $scope for you

ng-describe (magically) creates controller and $scope for you during testing

angular.module('TodoApp', [])
  .controller('TodoController', function ($scope) {
    $scope.todos = ['do this', 'do that'];
  });
ngDescribe({
  module: 'TodoApp',
  controller: 'TodoController',
  element: '<myDirective ....></myDirective>',
  parentScope: { ... },
  inject: ['foo', 'bar', '$q'],
  tests: function (deps) {
    // deps will have everything that ng-describe creates
    it('works', function () {
      ...
    });
  }
});

ng-describe can create everything for you

ngDescribe recreates everything from scratch before every unit test

ngDescribe({
  module: 'TodoApp',
  controller: 'TodoController',
  element: '<myDirective ....></myDirective>',
  parentScope: { ... },
  inject: ['foo', 'bar', '$q'],
  tests: function (deps) {
    beforeAll(function ...);
    beforeEach(function ...);

    it('works', function () {
      ...
    });

    afterEach(function ...);
    afterAll(function ...);
  }
});

ng-describe works with BDD callbacks

ng-describe removes boilerplate in great majority of unit test scenarios

Can we compute boilerplate / code ratio?

ngDescribe({ // Boilerplate
  module: 'TodoApp', // Code

original vs ng-describe

original boilerplate / code ratio = 2.7

describe('standard unit test', function () { // B
  beforeEach(angular.mock.module('TodoApp')); // C
  var $controller, $rootScope; // B
  beforeEach(inject(function (_$controller_, _$rootScope_) { // B
    $controller = _$controller_; // B
    $rootScope = _$rootScope_; // B
  }));
  it('creates a controller', function () { // B
    var scope = $rootScope.$new(); // B
    $controller('TodoController', { // C
      $scope: scope // B
    });
    expect(scope.todos.length).toEqual(2, 'has 2 todo items'); // C
  });
}); // B / C = 8 / 3 = 2.67

high overhead => larger unit tests

Trying to cram lots of assertions into single test

describe('standard unit test', function () {
  beforeEach(angular.mock.module('TodoApp'));
  var $controller, $rootScope;
  beforeEach(inject(function (_$controller_, _$rootScope_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
  }));
  it('creates a controller', function () {
    var scope = $rootScope.$new();
    $controller('TodoController', {
      $scope: scope
    });
    expect(Array.isArray(deps.TodoController.todos)).toBe(true);
    expect(scope.todos.length).toEqual(2, 'has 2 todo items');
    expect(...);
    expect(...);
  });
});

ng-describe is boilerplate buster

ngDescribe({ // B
  module: 'TodoApp', // C
  controller: 'TodoController', // C
  tests: function (deps) { // B
    it('creates a controller', function () { // B
      expect(deps.TodoController.todos.length).toEqual(2); // C
    });
  }
}); // B / C = 3 / 3 = 1.0

ng-describe boilerplate / code ratio = 1

Low overhead => smaller unit tests

ngDescribe({
  module: 'TodoApp',
  controller: 'TodoController',
  tests: function (deps) {

    it('has a list of todos', function () {
      expect(Array.isArray(deps.TodoController.todos)).toBe(true);
    });

    it('has two todos', function () {
      expect(deps.TodoController.todos.length).toEqual(2);
    });
  }
});

ng-describe goal

Remove unit-testing boilerplate to recreate the same "it just works" experience when writing AngularJS apps

Testing values / constants

angular.module('A', [])
  .value('foo', 'bar');

Need module "A"

Inject "foo"

// using value 'foo' from module 'A'
angular.module('useA', ['A'])
  .controller(function (foo, $scope) {
    $scope.name = foo;
  });

Testing values / constants

angular.module('A', [])
  .value('foo', 'bar');
ngDescribe({
  module: 'A',
  inject: 'foo',
  tests: function (deps) {
  }
    it('works', function () {
      expect(deps.foo).toEqual('bar');
    });
});

I want it!

npm install ng-describe --save-dev

// karma.conf.js
files: [
    'node_modules/angular/angular.js',
    'node_modules/angular-mocks/angular-mocks.js',
    'node_modules/ng-describe/dist/ng-describe.js',
    '<your source.js>',
    '<your specs.js>'
],
frameworks: ['mocha'], // or jasmine

If using Angular 1.x, have to use angular-mocks 1.x too

ng-describe only assumes BDD globals: describe, beforeEach, afterEach

Both Jasmine 1,2 and Mocha work beautifully

beforeAll, afterAll, fit, it.only, xit - they all work, ng-describe does not affect them

ng-describe API

ngDescribe(optionsObject);
// single function taking 
// a single options object

All properties in the options object: kensho/ng-describe#api

Mocha vs Jasmine

I prefer Mocha plus our own library of lazy assertions (la)

angular.module('A', [])
  .value('foo', 'bar');
ngDescribe({
  module: 'A',
  inject: 'foo',
  tests: function (deps) {
    it('has foo', function () {
      expect(deps.foo).toEqual('bar', 'foo is ' + deps.foo);





    });
  }
});
      // equivalent lazy-ass assertion
      la(deps.foo === 'bar', 'foo is', deps.foo);
      // la(condition, any, number, of, messages, objects, etc,
      //   to be included in the exception);

Recommended: Mocha + lazy-ass + check-more-types

angular.module('A', [])
  .value('foo', 'bar');
ngDescribe({
  module: 'A',
  inject: 'foo',
  tests: function (deps) {
    it('has foo', function () {
      la(check.has(deps, 'foo'), 'foo was injected');
      la(check.equal(deps.foo, 'bar'), 'foo is "bar"');
    });
  }
});

bahmutov/lazy-ass - lazy assertion message formatting

kensho/check-more-types - lots of predicates

CI tests ng-describe against angular 1.2, 1.3 and 1.4

using PhantomJs

on Node 0.10, 0.11, 0.12 and iojs

We also monitor the code quality using Codacy

Skip, only, verbose

Skip a suite of tests

ngDescribe({
  name: 'not ready yet',
  skip: true,
  tests: function (deps) { ... }
});

Skip, only, verbose

Focus on a suite of tests

ngDescribe({
  name: 'just this feature',
  only: true,
  tests: function (deps) { ... }
});

Skip, only, verbose

Skip + only

ngDescribe({
  name: 'logical contradiction',
  only: true,
  skip: true,
  tests: function (deps) { ... }
});
// ERROR!

Skip, only, verbose

If something does not work as expected

ngDescribe({
  name: 'let us find the problem',
  only: true,
  verbose: true,
  tests: function (deps) { ... }
});
// prints messages on suite setup, etc.

ng-describe examples

See all examples at kensho/ng-describe

Filter

Async

Directive

Mock $http

Mock everything

No time for spying examples

Testing $filter

ngDescribe({
  name: 'built-in filter',
  inject: '$filter',
  tests: function (deps) {





  }
});

la = lazy assertion, github.com/bahmutov/lazy-ass

    it('can convert to lowercase', function () {
      var lowercase = deps.$filter('lowercase');
      la(lowercase('Foo') === 'foo');
    });

Test async stuff

ngDescribe({
  inject: ['$q', '$rootScope'],
  tests: function (deps) {









  }
});

$rootScope - boilerplate digest cycle code!

    it('can be resolved', function (done) {
      deps.$q.when(42).then(function (value) {
        expect(value).toEqual(42);
        done();
      });
      // move promises along
      deps.$rootScope.$digest();
    });

Test async stuff

ngDescribe({
  inject: '$q',
  tests: function (deps) {
    it('can be resolved', function (done) {
      deps.$q.when(42).then(function (value) {
        expect(value).toEqual(42);
        done();
      });
      // move promises along
      deps.step();
    });
  }
});

using deps.step() shortcut

Test directive

angular.module('MyFoo', [])
  .directive('myFoo', function () {
    return {
      restrict: 'E',
      replace: true,
      template: '<span>{{ bar }}</span>'
    };
  });

Test directive

ngDescribe({
  name: 'MyFoo directive',
  modules: 'MyFoo',
  element: '<my-foo></my-foo>',
  // you can also setup custom parent scope
  tests: function (deps) {
     







  }
});
    it('can update DOM using binding', function () {
      la(check.has(deps, 'element'));
      var scope = deps.element.scope();
      scope.bar = 'bar';
      scope.$apply(); // or deps.step()
      la(deps.element.html() === 'bar');
    });

Testing directive without ng-describe

function setupElement(elementHtml) {
    var scope = dependencies.$rootScope.$new();
    angular.extend(scope, 
      angular.copy(options.parentScope));
    var element = angular.element(elementHtml);
    var compiled = dependencies.$compile(element);
    compiled(scope);
    dependencies.$rootScope.$digest();
    dependencies.element = element;
    dependencies.parentScope = scope;
}
if (options.element) {
  beforeEach(setupElement.bind(null, options.element));
}

Test directive: custom setup

// studyFlags directive makes HTTP call on init
ngDescribe({
  exposeApi: true,
  inject: '$httpBackend',
  tests: function (deps, describeApi) {
    











  });
});
    beforeEach(function () {
      deps.$httpBackend
        .expectGET('/api/foo/bar').respond(500);
    });
    beforeEach(function () {
      // now create an element ourselves
      describeApi.setupElement('<study-flags />');
    });
    it('created an element', function () {
      la(check.has(deps.element));
    });

Mock (fake) $http responses

ngDescribe({
  http: {
    get: {
      '/some/url': 42,
      '/some/other/url': [500, 'something went wrong']
    },
    post: {
      // you can use custom functions too
      '/some/post/url': function (method, url, data, headers) {
        return [200, 'ok'];
      }
    }
  },
  inject: '$http',
  tests: function (deps) {








  }
});

Shorthand notation for $httpBackend

    it('has meaning', function (done) {
        $http.get('/some/url').success(function (value) {
            expect(value).toEqual(42);
            done();
        });
        deps.step();
    });

Mock (fake) everything

angular.module('User', [])
  .value('greeting', function greeting() {
    return 'Hello';
  })
  .value('username', function username($http) {
    return $http.get('/user/name');
  });

Service with 1 http GET and one local method

Mock everything

angular.module('GreetUser', ['User'])
  .value('helloUser', function (greeting, username) {
    return username()
      .then(function (name) {
        return greeting() + ' ' + name + '!';
      });
  });

Use the service with "mixed" calls 

Server

User

GreetUser

GET

Mock everything: test

ngDescribe({
  module: 'GreetUser',
  inject: ['helloUser', '$rootScope'],
  tests: function (deps) {
    it('makes http request', function (done) {
      deps.helloUser()
        .then(function (value) {
          // hmm
        })
        .finally(done);
      deps.$rootScope.$apply();
    });
  }
});
ட default tests 
  ட ✘ makes http request FAILED
	Error: Unexpected request: GET /user/name
	No more request expected

Server

User

GreetUser

Test

GET

What should we mock?

ngDescribe({
  module: 'GreetUser',
  inject: ['helloUser', '$rootScope'],
  http: {
    get: {
      '/user/name': 'World'
    }
  },
  tests: function (deps) {
    it('makes http request', function (done) {
      ...
    });
  }
});

BAD!

Server

User

GreetUser

Test

GET

Mock 1st level deps

angular.module('GreetUser', ['User'])
  .value('helloUser', function (greeting, username) {
    return username()
      .then(function (name) {
        return greeting() + ' ' + name + '!';
      });
  });

Leave unchanged

Mock

Mock 1st level deps test

ngDescribe({
  module: 'GreetUser',
  inject: ['helloUser', '$rootScope'],
  mock: {
    User: {
      username: function ($q) {
        return $q.when('Test');
      }
    }
  },
  tests: function (deps) {
    it('does not make http requests', function (done) {
      deps.helloUser()
        .then(function (value) {
          la(value === 'Hello Test!');
        })
        .finally(done);
      deps.$rootScope.$apply();
    });
  }
});

replaces User.username with this code for testing

Mock for the target

ngDescribe({
  module: 'GreetUser',
  inject: ['helloUser', '$rootScope'],
  mock: {
    GreetUser: {
      username: function ($q) {
        return $q.when('Test');
      }
    }
  },
angular.module('GreetUser', ['User'])
  .value('helloUser', function (greeting, username) {
    return username()
      .then(function (name) {
        return greeting() + ' ' + name + '!';
      });
  });

What about spies?

Yes, both Jasmine spies and sinon.js are working fine

Bonus: user-friendly API

ngDescribe(optionsObject);
// single function taking 
// a single options object

User-friendly API (aliases)

ngDescribe({
  module: 'A'
});
// plural
ngDescribe({
  module: ['A', 'B']
});
// and
ngDescribe({
  modules: ['A', 'B']
});

Bonus: user-friendly API

ngDescribe({
  name: 'A',
  module: 'A',
  tests: function (deps) {
    ...
  });
})({
  name: 'A with B',
  modules: ['A', 'B'],
  tests: function (deps) {
    ...
  });
});

Bonus: user-friendly API

ngDescribe({
  name: 'A',
  module: 'A',
  tests: function (deps) {
    ...
  });
})({
  name: 'A with B',
  modules: ['A', 'B'],
  tests: function (deps) {
    ...
  });
});
})({

Butterfly: chained calls with options objects

ngDescribe({
  ...
})({
  ...
});
})({

Bonus: helpful assertion messages

it('does something', function () {
  ...
  expect(foo).toEqual('bar');
});
test "does something" failed
Error:

Bonus: helpful assertion messages

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 'expected foo to equal "bar"');
});
test "does something" failed
Error: expected foo to equal "bar"

What is the value of "foo"?

Bonus: helpful assertion messages

it('does something', function () {
  ...
  expect(foo).toEqual('bar', 
    'expected foo ' + foo + ' to equal "bar"');
});
test "does something" failed
Error: expected foo something to equal "bar"

hmm, boilerplate code again

helpDescribe(function () {
  ngDescribe({
    name: 'example',
    tests: function (deps) {
      it('does something', function () {
        ...
        la(deps.foo === 'bar');
      });
    }
  });
});
test "does something" failed
Error: condition [deps.foo === 'bar'] deps: { foo: ... }

lazy-ass-helpful works by on the fly rewriting test function's code

If you have existing tests - keep them, rewrite using ng-describe whenever a change is needed

Q: should we convert the existing unit tests to ng-describe?

Open source software - open an issue, discuss, submit a pull request

Q: can ng-describe do X?

Summary

Write fewer unit tests - target them better

Spend less time writing unit tests - use helper library like ng-describe

Home work

Read the companion blog post: glebbahmutov.com/blog/1-2-3-tested/

Read the kensho/ng-describe/README.md - lots of examples testing different things

Thank you

Kensho Boston / NYC / SF

Unit testing AngularJS

Unit testing AngularJS

By Gleb Bahmutov

Unit testing AngularJS

Testing Angular code requires too much boilerplate. In this presentation I will show ng-describe - a BDD helper that removes all extra code and allows to start unit testing in seconds. Presented at AngularNYC meetup in November 2015, video at https://www.youtube.com/watch?v=9DMuLsWzqwg

  • 8,634
Loading comments...

More from Gleb Bahmutov