JavaScript Testing
Noun
Unit Testing
- test framework + test runner: Jest
- test runner
- Karma. It runs test suite written in Jasmine, Mocha etc.
- cucumber
Feature: Serve coffee Coffee should not be served until paid for Coffee should not be served until the button has been pressed If there is no coffee left then money should be refunded
- test framework
- Jasmine
- Mocha:
beforeEach, describe, context, it
- assertion library
- Chai:
expect, equal, and exist
- power-assert "No API is the best API"
- Chai:
End to End Testing
- end to end testing framework: Protractor (run against real browser)
Which method comes from Mocha and Chai
We can distinguish between framework (Mocha) methods and assertion library (Chai) methods by looking at the contents of the it block. Methods outside the it
block are generally derived from the testing framework. Everything within the it
block is code coming from the assertion library. beforeEach, describe, context, it
, are all methods extending from Mocha. expect, equal, and exist
, are all methods extending from Chai.
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
$window.localStorage.removeItem('com.shortly');
});
it('should have a signup method', function() {
expect($scope.signup).to.be.a('function');
});
Jasmine Cheatsheet
Spy (for methods, can track calls or specify return value)
// Spy on existing method
beforeEach(function() {
foo = {
setBar: function(value) {
bar = value;
}
};
// Spy method
spyOn(foo, 'setBar');
// Return specified value
spyOn(foo, "getBar").and.returnValue(745);
// Throw error
spyOn(foo, "setBar").and.throwError("404");
});
// Use createSpy to stub a method
beforeEach(function() {
foo = {
setBar: jasmine.createSpy('setBar')
};
// Test
expect(foo.setBar).toHaveBeenCalled();
});
// Use createSpy to create bare method
beforeEach(function() {
let foo = jasmine.createSpy('foo');
// Test
foo('you can pass any', 'args');
expect(foo).toHaveBeenCalledWith('you can pass any', 'args');
});
// Use createSpyObj to create an object containing multiple methods
// Useful for mocking Angular service
beforeEach(function() {
let $window = jasmine.createSpyObj<angular.IWindowService>('$window', ['open', 'alert']);
(<jasmine.Spy>$window.alert).and.returnValue({});
// Test
$window.open();
expect($window.open).toHaveBeenCalled();
});
// Use spy to create an object containing both methods and property
// You cannot use createSpyObj as it's for methods only
beforeEach(function() {
foo = {
bar: {},
setBar: jasmine.createSpy('setBar')
};
// Test
foo.setBar();
expect(foo.bar).toEqual({});
expect(foo.setBar).toHaveBeenCalled();
});
Matcher
// Match with type
expect({}).toEqual(jasmine.any(Object));
expect(123).toEqual(jasmine.any(Number));
// Match function, useful to test callback function
expect(service.registerCallback).toHaveBeenCalledWith('key', jasmine.any(Function));
// Match partial object
expect(foo).toEqual(jasmine.objectContaining({
bar: "baz",
fooFunc: jasmine.any(Function)
}));
Tips
- Remember to call
$scope.$apply();
when testing promise. Why? - Call
done()
to instruct the spec is done for Asynchronous tests. Call the following methods when you testing $httpBackend. See doc and source code
afterEach(()=> { $httpBackend.verifyNoOutstandingRequest(); (<any>$httpBackend).verifyNoOutstandingExpectation(false); });
- Call $httpBackend.flush() or $scope.$digest(), but not both
- Any spec that calls a promise must execute expectations within a then or catch block (depending if the promise will resolve or reject)
Jasmine + Angular 1.x
Test component:
describe('My Tests', () => {
let $scope: angular.IScope,
$q: angular.IQService
$componentController: angular.IComponentControllerService;
beforeEach(() => {
angular.mock.module('module name');
inject(($injector : angular.auto.IInjectorService)=> {
$scope = $injector.get<angular.IRootScopeService>('$rootScope');
$q = $injector.get<angular.IQService>('$q');
$componentController = $injector.get<angular.IComponentControllerService>('$componentController');
});
});
it('should skip', () => {
pending();
});
});
Test Service itself: mock http
import {MyService} from './my-service';
import IHttpService = angular.IHttpService;
//import IQService = angular.IQService;
import IHttpBackendService = angular.IHttpBackendService;
//import IRootScopeService = angular.IRootScopeService;
describe('My Service', ()=> {
let myService: MyService,
$http: IHttpService,
$httpBackend: IHttpBackendService,
$q: IQService,
$rootScope: IRootScopeService;
beforeEach(()=> {
angular.mock.module('myModule');
inject(($injector: angular.auto.IInjectorService)=> {
$q = $injector.get<angular.IQService>('$q');
$http = $injector.get<angular.IHttpService>('$http');
$httpBackend = $injector.get<angular.IHttpBackendService>('$httpBackend');
$rootScope = $injector.get<angular.IRootScopeService>('$rootScope');
});
myService = new MyService($http);
});
it('Should make HTTP call', (done)=> {
$httpBackend.expectGET(`/URL/URL`).respond(200, 'fake data');
myService.getData()
.then(done);
$httpBackend.flush();
});
});
Test service in component:
let myService = jasmine.createSpyObj('myService', ['getData']);
(<jasmine.Spy>myService.getData).and.returnValue($q.when({}));
How to use mock data?
beforeEach(() => {
angular.mock.module('mock-jasmine/feature/mock-data.json');
inject(($injector: angular.auto.IInjectorService) => {
mockDataPerson = $injector.get('mockJasmineFeatureMockData');
});
});
// mock $language return value
spyOn($language, 'get').and.returnValue(na);
Mistakes to avoid:
- Always to remember to return a promise!
- In test, always do a $scope.$digest() after calling a promise so that it triggers the promise chain.