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