Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
SANS
GERARD
Spoken at 110 events in 27 countries
900
1.6K
Unit Tests
Assertion Libraries
Spies, Stubs
Test
Automation
Browsers
Coverage Reports
e2e Tests
Test
Runner
WebDriverJS
Selenium
Protractor
Unit tests
e2e Tests
Acceptance Tests
$ npm run tests $ npm run e2e
let calculator = {
add: (a, b) => a + b
};
describe('Calculator', () => {
it('should add two numbers', () => {
expect(calculator.add(1,1)).toBe(2);
})
})
fail(msg), pending(msg)
xdescribe, xit
fdescribe, fit
Test double functions that record calls, arguments and return values
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);
})
})
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);
})
})
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);
})
})
describe('Spies', () => {
it('should call fake function returning 42', () => {
spyOn(calculator, 'add').and.callFake((a,b) => 42);
expect(calculator.add(1,1)).toEqual(42);
})
})
describe('Spies', () => {
it('should throw with error', () => {
spyOn(calculator, 'add').and.throwError("Ups");
expect(() => calculator.add(1,1)).toThrowError("Ups");
})
})
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);
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);
})
})
import { TestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
import {Injectable} from '@angular/core';
@Injectable()
export class LanguagesService {
get() {
return ['en', 'es', 'fr'];
}
}
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);
});
});
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);
});
});
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/map';
@Injectable()
export class UsersService {
constructor(private http: HttpClient) { }
public get() {
return this.http.get('./src/assets/users.json')
.map(response => response.users);
}
}
describe('Service: UsersService', () => {
let service, http;
beforeEach(() => TestBed.configureTestingModule({
imports: [ HttpClientModule ],
providers: [ UsersService ]
}));
beforeEach(inject([UsersService, HttpClient], (s, h) => {
service = s;
http = h;
}));
[...]
describe('Service: UsersService', () => {
[...]
it('should return available users (LIVE)', done => {
service.get()
.subscribe({
next: res => {
expect(res.users).toBe(USERS);
expect(res.users.length).toEqual(2);
done();
}
});
});
});
describe('Service: UsersService', () => {
let service, httpMock;
beforeEach(() => TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ],
providers: [ UsersService ]
}));
beforeEach(inject([UsersService, HttpTestingController], (s, h) => {
service = s;
httpMock = h;
}));
afterEach(httpMock.verify);
[...]
describe('Service: UsersService', () => {
[...]
it('should return available users', done => {
service.get()
.subscribe({
next: res => {
expect(res.users).toBe(USERS);
expect(res.users.length).toEqual(2);
done();
}
});
httpMock.expectOne('./src/assets/users.json')
.flush(USERS);
});
});
import {Component, Input} from '@angular/core';
@Component({
selector: 'greeter', // <greeter name="Igor"></greeter>
template: `<h1>Hello {{name}}!</h1>`
})
export class Greeter {
@Input() name;
}
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;
});
}
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;
});
))
});
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!');
});
}));
}
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!');
}));
}
describe('Component: Greeter', () => {
let fixture, greeter, element, de;
//setup
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ Greeter ],
})
.compileComponents() // compile external templates and css
.then(() => {
TestBed.overrideTemplate(Greeter, '<h1>Hi</h1>');
fixture = TestBed.createComponent(Greeter);
greeter = fixture.componentInstance;
element = fixture.nativeElement;
de = fixture.debugElement;
});
))
});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ MyComponent ],
schemas: [ NO_ERRORS_SCHEMA ]
})
});
import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
describe("basic marbles", () => {
it("should support simple values as strings", marbles(m => {
const input = m.cold("--1--1|");
const expected = m.cold("--1--2|");
const output = input.pipe(map(x => x));
m.expect(output).toBeObservable(expected);
}));
});
});
const input = m.cold("--1--1|");
const expected = m.cold("--1--2|");
...
Cold Observables basic marbles should support simple values as strings
Error:
Expected
{"frame":20,"notification":{"kind":"N","value":"1","hasValue":true}}
{"frame":50,"notification":{"kind":"N","value":"1","hasValue":true}}
{"frame":60,"notification":{"kind":"C","hasValue":false}}
to deep equal
{"frame":20,"notification":{"kind":"N","value":"1","hasValue":true}}
{"frame":50,"notification":{"kind":"N","value":"2","hasValue":true}}
{"frame":60,"notification":{"kind":"C","hasValue":false}}
import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
describe("basic marbles", () => {
it("should support simple values as strings", marbles(m => {
const values = { a: 1, b: 2 };
const input = m.cold("--a--a|", values);
const expected = m.cold("--b--b|", values);
const output = input.pipe(map(x => x+1));
m.expect(output).toBeObservable(expected);
}));
});
});
import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
describe("basic marbles", () => {
it("should support custom errors (symbols)", marbles(m => {
const values = { a: 1 };
const input = m.cold("--a#", values, new Error('Ups'));
const expected = m.cold("--a#", values, new Error('Ups'));
const output = input.pipe(map(x => x));
m.expect(output).toBeObservable(expected);
}));
});
});
import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
describe("basic marbles", () => {
it("should support custom Observables", marbles(m => {
const input = throwError(new Error('Ups')));
const expected = m.cold("#", undefined, new Error('Ups'));
const output = input.pipe(map(x => x));
m.expect(output).toBeObservable(expected);
}));
});
});
import {marbles} from "rxjs-marbles";
describe("Hot Observables", () => {
describe("Subscriptions", () => {
it("should support basic subscriptions", marbles(m => {
const values = { a: 1, b: 2 };
const input = m.hot( "--a^-a|", values);
const expected = m.cold( "--b|", values);
const output = input.pipe(map(x => x+1));
m.expect(output).toBeObservable(expected);
}));
});
});
import {marbles} from "rxjs-marbles";
describe("Hot Observables", () => {
describe("Subscriptions", () => {
it("should support testing subscriptions", marbles(m => {
const values = { a: 1, b: 2 };
const input = m.hot( "--a^-a|", values);
const subs = "^-!";
const expected = m.cold( "--b|", values);
const output = input.pipe(map(x => x+1));
m.expect(output).toBeObservable(expected);
m.expect(input).toHaveSubscriptions(subs);
}));
});
});
const values = { a: 1, b: 2 };
const input = m.hot( "--a^-a|", values);
const subs = "^-!";
const expected = m.cold( "--b|", values);
...
Hot Observables Subscriptions should support complex subscriptions
Error:
Expected
{"subscribedFrame":0,"unsubscribedFrame":30}
to deep equal
{"subscribedFrame":0,"unsubscribedFrame":20}
// actions/spinner.actions.spec.ts
import { SpinnerShow, SpinnerHide, SpinnerActionTypes } from './spinner.actions';
describe('SpinnerShow', () => {
it('should create an instance', () => {
const action = new SpinnerShow();
expect(action).toBeTruthy();
expect(action.type).toBe(SpinnerActionTypes.Show);
});
});
describe('SpinnerHide', () => {
it('should create an instance', () => {
const action = new SpinnerHide();
expect(action).toBeTruthy();
expect(action.type).toBe(SpinnerActionTypes.Hide);
});
});
// reducers/spinner.reducer.spec.ts
describe('Spinner Reducer', () => {
it('should return the initial state', () => {
const action = { type: '🚀' } as any;
const result = reducer(initialState, action);
expect(result).toBe(initialState);
expect(initialState).toBe(false);
});
});
// reducers/spinner.reducer.spec.ts
describe('Spinner Reducer', () => {
it('should handle SpinnerShow Action', () => {
const action = new SpinnerShow();
const result = reducer(initialState, action);
expect(result).toBe(true);
});
it('should handle SpinnerHide Action', () => {
const action = new SpinnerHide();
const result = reducer(initialState, action);
expect(result).toBe(false);
});
});
// reducers/selectors.spec.ts
describe('Selectors', () => {
let adapter : EntityAdapter<Todo>;
let initialState : any;
let t: Array<Todo> = [
{ id: 1, text: 'Learn French', completed: false },
{ id: 2, text: 'Try Poutine', completed: true }
];
beforeAll(() => {
adapter = createEntityAdapter<Todo>();
initialState = adapter.getInitialState();
})
})
// reducers/selectors.spec.ts
describe('Selectors', () => {
describe('getFilteredTodos', () => {
it('should return only active todos', () => {
const todos = adapter.addMany(t, initialState);
const state: TodosState = {
todos,
currentFilter: "SHOW_ACTIVE"
}
expect(getFilteredTodos(state)).toEqual([t[0]]);
})
})
})
// loading/loading.component.spec.ts
describe('LoadingComponent', () => {
let fixture, loading, element, de;
// setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ LoadingComponent ]
});
fixture = TestBed.createComponent(LoadingComponent);
loading = fixture.componentInstance;
element = fixture.nativeElement;
de = fixture.debugElement;
});
})
// loading/loading.component.spec.ts
describe('LoadingComponent', () => {
let fixture, loading, element, de;
// specs
it('should render Spinner', () => {
loading.loading = true;
fixture.detectChanges(); //trigger change detection
fixture.whenStable().then(() => {
expect(element.querySelector('svg')).toBeTruthy();
});
});
})
Components, Directives, Pipes
Services, Http, MockBackend
Router, Observables, Spies
Animations
Marble Testing