Software Engineer Frontend / Java
sec
6000+ tests
1100+ tests
sec
stack
framework
library
environment
dependencies
Angular JS
Grunt
Karma
Jasmine
Chrome
Node.js
Grunt
Mocha
Chai
describe('TodoService', function () {
var service;
beforeEach(module('main'));
beforeEach(module(function ($provide) {
$provide.constant('initialTodos', []);
}));
beforeEach(inject(function (_TodoService_) {
service = _TodoService_;
}));
it('should contain empty todos after initialization', function() {
expect(service.todos.length).toBe(0);
});
it('should add todo', function () {
service.addTodo('Finish example project');
expect(service.todos.length).toBe(1);
expect(service.todos[0].label).toBe('Finish example project');
});
// ... more tests
});
INITIALIZED ANGULAR JS
DEPENDENCY INJECTION
MECHANISM
WORKS BY REGISTERING EVERYTHING ON GLOBAL ANGULAR OBJECT
BY USING EXPOSED API LIKE MODULE, CONTROLLER, FACTORY…
// global angular object
angular
// registration APIs
.module('app', [])
.controller('MyController', function() {})
.factory('MyFactory', function() {})
.service('MyService', function() {});
// directive, provider ...
describe('TodoService', function () {
var service;
beforeEach(module('main'));
beforeEach(module(function ($provide) {
$provide.constant('initialTodos', []);
}));
beforeEach(inject(function (_TodoService_) {
service = _TodoService_;
}));
it('should contain empty todos after initialization', function() {
expect(service.todos.length).toBe(0);
});
it('should add todo', function () {
service.addTodo('Finish example project');
expect(service.todos.length).toBe(1);
expect(service.todos[0].label).toBe('Finish example project');
});
// ... more tests
});
initialize app context
mock dependencies
get reference to tested object
describe('TodoService', function () {
var scope, $rootScope, $controller;
beforeEach(module('main'));
beforeEach(module(function($provide) {
var TodoServiceMock = {} // mock TodoServie
$provide.value('TodoService', TodoServiceMock);
}));
beforeEach(inject(function (_$controller_, _$rootScope_, _TodoService_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$controller('TodoController', {
$scope: scope
TodoService: _TodoService_
});
}));
it('should have initial todos', function() {
expect(scope.todos.length).toBe(1);
});
// ... more tests
});
initialize app context
mock dependencies
get reference to tested object
describe('TodoService', function () {
var directive, scope, $compile, $rootScope;
beforeEach(module('main'));
beforeEach(inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.new();
var element = angular.element('<div todo-component></div>');
var directive = compile(element)(scope);
scope.$digest();
}));
it('should have initial todos', function() {
var button = directive.find('#remove-done-todos');
button.triggerHandler('click');
scope.$digest();
expect(scope.todos.length).toEqual(0);
});
// ... more tests
});
initialize app context
compile template
get reference to element ?!
Spec.execute
Angular context module(‘app’) must be instantiated before every test
Context must be available to be able to do any testing at all even though the functionality may be in form of pure functions / classes not using any Angular specific API. Without Angular context you can’t get access (reference) to your controllers / services.
Even with Angular context, it is still hard to get references to some units (directive's controller)
Compile inline template, inject scope, fire events on DOM elements, vs
registering directive's controller as .controller() in Angular directly
Angular and all other used libraries must be included during testing
...so it is even possible to instantiate Angular context
Angular context can grow
It’s creation will then consume considerable amount of time for every test file (beforeEach(module('app')))
* it is possible to split application into submodules and instantiate only the deepest submodule but this adds overhead of registering and maitaining of submodules and you still may need to instantiate whole app to get references to cross-cutting concerns like logging, exception handling or other infrastructure services
*
Works with any module system out there, but with the ES6 being officially released and also adopted by Typescript I would say it is the choice which makes most sense right now
// beautiful ES6 import statement
import TodoService from './todo.service.js';
=
2 DEPENDENCY INJECTION MECHANISMS IN ONE PROJECT
VS
import { assert } from 'chai';
import TodoService from './todo.service.js';
let service;
describe('TodoService', function() {
beforeEach(function() {
service = TodoService();
});
it('should contain empty todos after initialization', function () {
assert.equal(service.todos.length, 0);
});
it('should add todo', function () {
service.addTodo('Finish example project');
assert.equal(service.todos.length, 1);
assert.equal(service.todos[0].label, 'Finish example project');
assert.equal(service.todos[0].done, false);
});
});
As you could see, test just imports the service and well… tests it!
No Angular context or API whatsoever, just as it should be because we are unit testing the service functionality not the Angular’s dependency injection mechanism.
import * as _ from 'lodash';
export default function TodoService(initialTodos) {
const todos = initialTodos;
return {
todos,
addTodo,
toggleTodo,
removeDoneTodos
};
function addTodo(label) {
let todo = {
label,
done: false
};
todos.push(todo);
}
// other methods ...
}
import angular from 'angular';
import TodoService from './services/todo.service';
export default angular
.module('main.app.feature-b', [])
.factory('TodoService', TodoService)
.name;
// other controllers, directives, etc..
// imports skipped for increased brevity
describe('TodoComponent with mocked service (unit test)', function() {
beforeEach(function() {
let initialTodos = [];
let TodoServiceInstance = TodoService(initialTodos);
mockTodoService = sinon.mock(TodoServiceInstance);
component = new TodoComponent(TodoServiceInstance);
});
afterEach(function() {
mockTodoService.restore();
});
it('should add todo', function () {
mockTodoService
.expects('addTodo')
.once()
.withArgs('Finish example project');
component.label = 'Finish example project';
component.addTodo();
mockTodoService.verify();
});
});
import { assert } from 'chai';
import TodoComponent from './todo-component.js';
import TodoService from '../services/todo.service.js';
let component;
describe('TodoComponent with real service (Integration test)', function() {
beforeEach(function() {
let initialTodos = [];
let todoService = TodoService(initialTodos);
component = new TodoComponent(todoService);
});
it('should add todo', function () {
component.label = 'Finish example project';
component.addTodo();
assert.equal(component.label, '');
assert.equal(component.todos.length, 1);
assert.equal(component.todos[0].label, 'Finish example project');
assert.equal(component.todos[0].done, false);
});
});
Karma / Jasmine have proven to be great at instantiating of whole Angular JS application context in a browser
how the standard Angular JS Karma / Jasmine tests work
what is Angular JS context
why it exits and how it relates to testing
how to use (ES6) modules to circumvent Angular JS context
how to unit test functionality using Mocha
how to split code between implementation and registration into Angular JS context
when and why to use Karma / Jasmine integration tests
Software Engineer Frontend / Java