JavaScript Testing
Madhan Ganesh
Why Test?
Because Writing the test is easier than running the application
Principles
Predictable
Single Responsibility
Self Documenting
Tests Behaviour
Example to discuss
describe('add 2 numbers', function() {
it('should return sum of both operands', function() {
expect(calculator.add(1, 1)).to.be(2);
});
});
Predictable
One thing at a time
Self documenting
Test behaviour
Excuses
It is working, why to do more?
It will take too long and I need to know more!
My code just works fine
Unit test do not catch any bugs
Who cares? no business value
Oh! takes time to run them
If writing unit test is harder, listen carefully to your code...
Tightly couples components
Singletons
Mixed Concerns
Asynchronous functions
Tools.. tools & tools
Karma
Jasmine
Sinon
Chai
Mocha
Protactor
Grunt
ngMock
Testing doubles with Jasmine spies
A test double is an object that acts and is used in place of another object.
Test double
Object under test
- Count of calls on a function
- The ability to specify a return value (stub a return value)
- The ability to test arguments
Count of calls on a function
// Arrange
jasmine.spyOn(calculator, 'add');
// Act
calculator.add(2, 3);
calculator.add(3, 4);
// Assert
assert(calculator.add.count).is(2);
var calculator = {
add: function(a, b) {
var result = a + b;
return result;
}
};
Stub a return value
// Arrange
jasmine.spyOn(wordSource, 'getWord')
.and
.returnValue('simple');
// Act
var ret = wordManager.captilize();
// Assert
expect(ret).toBe('SIMPLE');
var wordManager = {
capitalize: function(wordSource) {
var word = wordSource.getWord(); // MY DEPENDENCY
return word.toUpperCase(); // MY LOGIC
}
};
Testing arguments
// Arrange
wordManager.setPrefix('Mr.');
jasmine.spyOn(wordSource, 'getWord');
// Act
var ret = wordManager.captilize();
// Assert
var callArgs = wordSource.getWord.call.argsFor(0);
expect(callArgs).toBe('Mr.');
var wordManager = {
// .. logic to set 'myprefix'
capitalize: function(wordSource) {
var word = wordSource.getWord('myprefix '); // MY DEPENDENCY
return word.toUpperCase(); // MY LOGIC
}
};
Asynchronous Testing
- setTimeout & setTimeInterval
- UI effects
- Ajax calls
Jasmine 2 ways
- runs() & waitsFor()
- Clock.useMock()
runs() & waitsFor()
var Calculator =
function(displayElement) {
this.el = $(displayElement);
};
Calculator.proptotype.hideResult =
function(callback) {
this.$el
.fadeOut(1000, callback);
}
describe('Calculator', function() {
var calc, el;
beforEach(function() {
el = $('<div>some content</div>');
calc = new Calculator(el);
});
it('should hide the result', function() {
// Arrange
var flag = false;
var callback = function() {
flag = true;
});
// Act
runs(function() {
calc.hideResult(callback);
});
waitsFor(function() {
return flag;
}, 'flag to be true', 1100);
// Assert
runs(function() {
expect(el.css('display')).toBe('none');
});
});
});
Clock.useMock()
function hideAfterSometime() {
setTimeout(function() {
hide = true;
}, 1000);
}
beforeEach(function() {
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('should hide after 1 second', function() {
// Act
hideAfterSometime();
jasmine.clock().tick(1001);
// Assert
expect(hide).toBe(true);
});
Jasmine Spies
- Built for mocking single functions
- Inbuilt matchers
- Supports:
- Replacement mocking
- Pass-through mocking
function readFile(fileName, onsuccess) {
var f = new FileReader(fileName);
var data = f.readFile();
onsuccess(data);
}
Spy Function
it('should invoke callback after file is read', function() {
// Arrange
var spyCallback = jasmine.createSpy('successcb');
// Act
readFile('some.txt', spyCallback);
// Assert
expect(spyCallback).toHaveBeenCalled();
});
function WorkitemsManager(worklistService) {
this.exams = [];
this.readExams = function() {
this.exams = worklistService.getExams();
}
}
Spy Method - mock return
it('should invoke callback after file is read', function() {
// Arrange
var exams = ['abc'];
var worklistService = new WorklistService();
spyOn(worklistService, 'getExams').andReturn(exams);
// Act
var workitemsManager = new WorkitemsManager(worklistService);
workitemManager.readExams();
// Assert
expect(workitemsManager.exams).toEqual(exams);
});
function WorkitemsManager(worklistService) {
this.exams = [];
this.readExams = function() {
this.exams = worklistService.getExams();
}
}
Spy Method - and call through
it('should invoke callback after file is read', function() {
// Arrange
var worklistService = new WorklistService();
var spy = spyOn(worklistService, 'getExams').andCallThrough();
// Act
var workitemsManager = new WorkitemsManager(worklistService);
workitemManager.readExams();
// Assert
expect(spy).toHaveBeenCalled();
});
function WorkitemsManager(worklistService) {
this.exams = [];
this.readExams = function() {
try {
this.exams = worklistService.getExams();
} catch(ex) {
this.exams = [];
}
}
}
Spy Method - and throw
it('should invoke callback after file is read', function() {
// Arrange
var worklistService = new WorklistService();
spyOn(worklistService, 'getExams').andThrow(new Error('problem'));
// Act
var workitemsManager = new WorkitemsManager(worklistService);
workitemManager.readExams();
// Assert
expect(workitemManager.exams).toBe([]);
});
module.directive('spinner', function (spinnerSvc) {
return {
link: function (scope, element) {
scope.$on('showspinner', function () {
spinnerSvc.spin(element[0]);
});
scope.$on('stopspinner', function () {
spinnerSvc.stop(element[0]);
});
}
};
});
Spy Object
it('should show spinner on "showspinner" event', function () {
// Arrange
spinnerSvcMock = jasmine.createSpyObj('spinnerSvc', ['spin', 'stop']);
// Act
scope.$broadcast('showspinner');
// Assert
expect(spinnerSvcMock.spin).toHaveBeenCalledWith(element[0]);
});
function WorkitemsManager(worklistService) {
this.exams = [];
this.readExams = function() {
worklistService.getExams(function(exams) {
this.exams = exams;
});
}
}
Spy Method - mock callback
it('should invoke callback after file is read', function() {
// Arrange
var exams = ['abc'];
var worklistService = new WorklistService();
spyOn(worklistService, 'getExams').andCallFake(function(callback) {
callback(exams);
});
// Act
var workitemsManager = new WorkitemsManager(worklistService);
workitemManager.readExams();
// Assert
expect(workitemsManager.exams).toEqual(exams);
});
Jasmine Spy Matchers
Jasmine Documentation
Testing Angular Objects
- Controller
- Service
- Directive
Setup
describe('HeaderContoller', function () {
beforeEach(module('app'));
beforeEach(module(function($provide) {
// inject mock services
}));
beforeEach(inject(function ($rootScope, $controller) {
// create scope, controllers and mocks
});
it('should test something', function() {
// Arrange
// Act
// Assert
});
...
});
Start here
Load Module
Customize DI
Mock objects
Tests..
(function () {
'use strict';
describe('Header Controller', function () {
var rootScope,
controllerFactory,
controller,
scope;
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $controller) {
rootScope = $rootScope;
scope = $rootScope.$new();
controllerFactory = $controller;
controller = controllerFactory('HeaderCtrl', {
$scope: scope
});
}));
it('it should ', function () {
});
});
})();
Testing Controller
(function() {
describe('worklist service', function () {
var worklistSvc;
beforeEach(module('app'));
beforeEach(inject(function (_worklistSvc_) {
worklistSvc = _worklistSvc_;
}));
it('', function() {
});
});
})()
Testing Service
(function() {
'use strict';
describe('Datepicker Directive', function() {
var element, scope, compile;
// setup - before each
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element('{{!cursor}}');
$compile(element)(scope);
scope.$digest();
compile = $compile;
}));
// Tests start here
});
})();
Testing Directive
Benefits of Unit Test
- Safety Net
- Save Time
- Makes it ease
- Makes development fun!
Demo
JavaScript Testing
By Madhan Ganesh L
JavaScript Testing
process, tools, samples on JavaScript testing
- 945