Angular
Testing Recipes
slides.com/gerardsans | @gerardsans
Google Developer Expert
Master of Ceremonies
Blogger
International Speaker
Angular Trainer (v4+)
Community Leader
850
1K
Angular In Flip Flops
Introduction
Unit Tests
Assertion Libraries
Spies, Stubs
Test
Automation
Browsers
Coverage Reports
e2e Tests
Test
Runner
Testing Architecture
WebDriverJS
Selenium
Protractor
Overview
- Does this method work?
- Does this feature work?
- Does this product work?
Unit tests
e2e Tests
Acceptance Tests
Angular 2 Rapid Development
- app.component.ts
- app.component.spec.ts
- app.e2e.ts
Filename conventions
$ npm run tests $ npm run e2e
Tools Online
Mocks vs Stubs
Mocks
- Used to replace Complex Objects/APIs
- Examples:
- MockBackend
- MockEventEmitter
- MockLocationStrategy
Stubs
- Used to cherry pick calls and change their behaviour for a single test
- When to use:
- control behaviour to favour/avoid certain path
Jasmine
Main Concepts
- Suites describe('', function)
- Specs it('', function)
- Expectations and Matchers
- expect(x).toBe(expected)
- expect(x).toEqual(expected)
Basic Test
let calculator = {
add: (a, b) => a + b
};
describe('Calculator', () => {
it('should add two numbers', () => {
expect(calculator.add(1,1)).toBe(2);
})
})
Setup and teardown
-
beforeAll (once)
- beforeEach (many)
- afterEach (many)
- afterAll (once)
Useful techniques
- Nesting suites and using scopes
-
Utility APIs
-
fail(msg), pending(msg)
-
- Disable
-
xdescribe, xit
-
- Focused
-
fdescribe, fit
-
Jasmine Spies
Test double functions that record calls, arguments and return values
Tracking Calls
describe('Spies', () => {
let calculator = { add: (a,b) => a+b };
it('should track calls but NOT call through', () => {
spyOn(calculator, 'add');
let result = calculator.add(1,1);
expect(calculator.add).toHaveBeenCalled();
expect(calculator.add).toHaveBeenCalledTimes(1);
expect(calculator.add).toHaveBeenCalledWith(1,1);
expect(result).not.toEqual(2);
})
})
Calling Through
describe('Spies', () => {
it('should call through', () => {
spyOn(calculator, 'add').and.callThrough();
let result = calculator.add(1,1);
expect(result).toEqual(2);
//restore stub behaviour
calculator.add.and.stub();
expect(calculator.add(1,1)).not.toEqual(2);
})
})
Set return values
describe('Spies', () => {
it('should return value with 42', () => {
spyOn(calculator, 'add').and.returnValue(42);
let result = calculator.add(1,1);
expect(result).toEqual(42);
})
it('should return values 1, 2, 3', () => {
spyOn(calculator, 'add').and.returnValues(1, 2, 3);
expect(calculator.add(1,1)).toEqual(1);
expect(calculator.add(1,1)).toEqual(2);
expect(calculator.add(1,1)).toEqual(3);
})
})
Error handling
describe('Spies', () => {
it('should throw with error', () => {
spyOn(calculator, 'add').and.throwError("Ups");
expect(() => calculator.add(1,1)).toThrowError("Ups");
})
})
Creating Spies
describe('Spies', () => {
it('should be able to create a spy manually', () => {
let add = jasmine.createSpy('add');
add();
expect(add).toHaveBeenCalled();
})
})
// usage: create spy to use as a callback
// setTimeout(add, 100);
Creating Spies
describe('Spies', () => {
it('should be able to create multiple spies manually', () => {
let calculator = jasmine.createSpyObj('calculator', ['add']);
calculator.add.and.returnValue(42);
let result = calculator.add(1,1);
expect(calculator.add).toHaveBeenCalled();
expect(result).toEqual(42);
})
})
Angular Testing
Testing APIs
- inject,TestBed
- async
- fakeAsync/tick
Setup
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
Testing a Service
import {Injectable} from '@angular/core';
@Injectable()
export class LanguagesService {
get() {
return ['en', 'es', 'fr'];
}
}
Testing a Service
describe('Service: LanguagesService', () => {
//setup
beforeEach(() => TestBed.configureTestingModule({
providers: [ LanguagesService ]
}));
//specs
it('should return available languages', inject([LanguagesService], service => {
let languages = service.get();
expect(languages).toContain('en');
expect(languages).toContain('es');
expect(languages).toContain('fr');
expect(languages.length).toEqual(3);
});
});
refactoring inject
describe('Service: LanguagesService', () => {
let service;
beforeEach(() => TestBed.configureTestingModule({
providers: [ LanguagesService ]
}));
beforeEach(inject([LanguagesService], s => {
service = s;
}));
it('should return available languages', () => {
let languages = service.get();
expect(languages).toContain('en');
expect(languages).toContain('es');
expect(languages).toContain('fr');
expect(languages.length).toEqual(3);
});
});
Asynchronous Testing
Asynchronous APIs
- Jasmine.done
- async
- fakeAsync/tick
Http Service
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';
@Injectable()
export class LanguagesServiceHttp {
constructor(private http:Http) { }
get(){
return this.http.get('api/languages.json')
.map(response => response.json());
}
}
Testing Http Service (mock)
describe('MockBackend: LanguagesServiceHttp', () => {
let mockbackend, service;
beforeEach(() => TestBed.configureTestingModule({
providers: [
BaseRequestOptions,
MockBackend,
{ provide: Http,
useFactory: (backend, options) => new Http(backend, options),
deps: [MockBackend, BaseRequestOptions] },
LanguagesServiceHttp
]
}));
beforeEach(inject([MockBackend, LanguagesServiceHttp], (m, s) => {
mockbackend = m;
service = s;
}))
Testing MockBackend
it('should return mocked response (sync)', () => {
//setup mock response
let response = ["ru", "es"];
mockbackend.connections.subscribe(connection => {
connection.mockRespond(new Response({body: JSON.stringify(response)}));
});
//check expectations
service.get().subscribe(languages => {
expect(languages).toContain('ru');
expect(languages).toContain('es');
expect(languages.length).toBe(2);
});
});
Components Testing
Greeter Component
import {Component, Input} from '@angular/core';
@Component({
selector: 'greeter', // <greeter name="Igor"></greeter>
template: `<h1>Hello {{name}}!</h1>`
})
export class Greeter {
@Input() name;
}
Testing Fixtures (sync)
describe('Component: Greeter', () => {
let fixture, greeter, element, de;
//setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ Greeter ]
});
fixture = TestBed.createComponent(Greeter);
greeter = fixture.componentInstance;
element = fixture.nativeElement;
de = fixture.debugElement;
});
}
Testing Fixtures (async)
describe('Component: Greeter', () => {
let fixture, greeter, element, de;
//setup
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ Greeter ],
})
.compileComponents() // compile external templates and css
.then(() => {
fixture = TestBed.createComponent(Greeter);
greeter = fixture.componentInstance;
element = fixture.nativeElement;
de = fixture.debugElement;
});
))
});
Using Change Detection
describe('Component: Greeter', () => {
it('should render `Hello World!`', async(() => {
greeter.name = 'World';
//trigger change detection
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(element.querySelector('h1').innerText).toBe('Hello World!');
expect(de.query(By.css('h1')).nativeElement.innerText).toBe('Hello World!');
});
}));
}
Using fakeAsync
describe('Component: Greeter', () => {
it('should render `Hello World!`', fakeAsync(() => {
greeter.name = 'World';
//trigger change detection
fixture.detectChanges();
//execute all pending asynchronous calls
tick();
expect(element.querySelector('h1').innerText).toBe('Hello World!');
expect(de.query(By.css('h1')).nativeElement.innerText).toBe('Hello World!');
}));
}
Shallow Testing
NO_ERRORS_SCHEMA
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ MyComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
})
});
e2e Testing
Main concepts
- Test features instead of methods
- Test as final user no Mocking
- Run on multiple browsers
- Complex to create/debug
- Resource intensive (slow)
Protractor
- Automate browser testing
- WebDriverJS Wrapper
- ControlFlow
- Deals with async code (zones)
Protractor
- Browser
- browser.driver
- browser.get(url)
- DOM
- by.id('user')
- element(selector).getText()
More?
Examples covering
-
Components, Directives, Pipes
-
Services, Http, MockBackend
-
Router, Observables
-
Spies
Blog Post
Thanks!
Дякую
Angular Testing Recipes (v4+)
By Gerard Sans
Angular Testing Recipes (v4+)
In this talk, we will cover the most common testing scenarios to use while developing rock solid Angular Applications, like: Components, Services, Http and Pipes; but also some less covered areas like: Directives, the Router and Observables. We will provide examples for using TestBed, fixtures, async and fakeAsync/tick while recommending best practices.
- 4,515