Angular Workshop
Testing
Contents
A simple jasmine test suite
describe("A resource",() => {
const resource;
beforeEach(() => {
resource = new Resource();
resource.allocateSpace();
});
afterEach(() => {
resource.free();
});
it("should have allocated 100 units of space",() => {
expect(resource.space).toEqual(100);
});
it("should be connected to at least one device",() => {
expect(resource.isConnected).toEqual(true);
expect(resource.connections.length).toBeGreaterThan(0);
});
});
Jasmine Matchers
A simple jasmine spy
describe('A Person', () => {
let fakePerson;
beforeEach( () => {fakePerson = new Person();})
it('should call the sayHello() function', () => {
spyOn(fakePerson, 'sayHello');
fakePerson.helloSomeone('world');
expect(fakePerson.sayHello).toHaveBeenCalled();
});
it('should greet the world', () => {
spyOn(fakePerson, 'helloSomeone');
fakePerson.helloSomeone('world');
expect(fakePerson.helloSomeone).toHaveBeenCalledWith('world')
});
});
class Person {
helloSomeone(toGreet) {
return `${this.sayHello()} ${toGreet}`;
};
sayHello() {
return 'Hello';
};
}
Advanced spy behaviour
// Actually calling the method
spyOn(fakePerson, 'sayHello').and.callThrough();
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Call a different function
spyOn(fakePerson, 'sayHello').and.callFake((arguments, can, be, received) => ...);
// Get number of calls
spyOn(fakePerson, 'sayHello')
expect(fakePerson.sayHello.calls.count()).toBe(3)
fakePerson.sayHello.calls.reset() // reset the counts
// Create a "bare" spy
spy = jasmine.createSpy('whatAmI');
expect(spy).toHaveBeenCalled();
// Create a spy object
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
Async support
describe("asynchronous specs",() => {
it("should take a long time",(done) => {
setTimeout(function() {
done();
}, 9000);
});
})
Setting up Karma with Jasmine and TypeScript
import {Component} from '@angular/core';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message: string = '';
constructor() {}
setMessage(newMessage: string) {
this.message = newMessage;
}
clearMessage() {
this.message = '';
}
}
Verifying Methods and Properties - isolated test
import {GreetComponent} from './greet.component';
describe('Testing message state in greet.component', () => {
let greetComponent: GreetComponent;
beforeEach(() => {
greetComponent = new GreetComponent();
});
it('should set new message', () => {
greetComponent.setMessage('Testing');
expect(greetComponent.message).toBe('Testing');
});
it('should clear message', () => {
greetComponent.clearMessage();
expect(greetComponent.message).toBe('');
});
});
Verifying Methods and Properties - isolated test
describe('Testing GreetComponent', () => {
let component: GreetComponent;
let fixture: ComponentFixture<GreetComponent>;
let debugElement: DebugElement;
let element: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
imports: [ /* HttpModule, etc. */ ],
providers: [ /* { provide: ServiceA, useClass: TestServiceA } */ ]
});
fixture = TestBed.createComponent(GreetComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.query(By.css('h1'));
element = debugElement.nativeElement;
});
});
Testing with Angular testing utilities - setup
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { GreetComponent } from './greet.component';
Testing with Angular testing utilities - setup
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
});
fixture = TestBed.createComponent(GreetComponent);
Testing with Angular testing utilities - setup
TestBed
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
});
fixture = TestBed.createComponent(GreetComponent);
Testing with Angular testing utilities - setup
TestBed
TestBed.overrideComponent(SomeComponent, {
set: {
template: '<div>Overridden template here</div>'
// ...
}
});
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
});
fixture = TestBed.createComponent(GreetComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.query(By.css('h1'));
element = debugElement.nativeElement;
Testing with Angular testing utilities - setup
createComponent()
Testing with Angular testing utilities - the tests
import {Component} from '@angular/core';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message: string = 'Hello';
constructor() {}
setMessage(newMessage: string) {
this.message = newMessage;
}
clearMessage() {
this.message = '';
}
}
it('should display original greet', () => {
fixture.detectChanges();
expect(element.textContent).toContain(component.message);
});
it('should display a different test greet', () => {
component.setMessage('Test Greet');
fixture.detectChanges();
expect(element.textContent).toContain('Test Greet');
});
it('should clear the message', () => {
fixture.detectChanges();
component.clearMessage();
fixture.detectChanges();
expect(element.textContent).toBe('');
});
Testing with Angular testing utilities - the tests
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Testing with Angular testing utilities - the tests
detectChanges()
import { async } from '@angular/core/testing';
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
})
.compileComponents(); // compile template and css
}));
Testing with Angular testing utilities - external templates
import { async } from '@angular/core/testing';
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
})
.compileComponents();
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(GreetComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.query(By.css('h1'));
element = debugElement.nativeElement;
});
Testing with Angular testing utilities - external templates
import {Component} from '@angular/core';
import {UserService} from './user.service';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message: string = 'Hello';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.message = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
Testing Components with a dependency
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ] // Don't provide the real service!
providers: [ {provide: UserService, useValue: userServiceStub } ]
});
userService = TestBed.get(UserService);
Testing Components with a dependency - stubbing the service
// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);
Testing Components with a dependency - accessing the service
// UserService from the root injector
userService = TestBed.get(UserService);
it('stub object and injected UserService should not be the same', () => {
expect(userServiceStub === userService).toBe(false);
userServiceStub.isLoggedIn = false;
expect(userService.isLoggedIn).toBe(true);
});
it('should welcome the user', () => {
fixture.detectChanges();
const content = element.textContent;
expect(content).toContain('Welcome', '"Welcome ..."');
expect(content).toContain('Test User', 'expected name');
});
it('should welcome "Bubba"', () => {
userService.user.name = 'Bubba';
fixture.detectChanges();
expect(element.textContent).toContain('Bubba');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false;
fixture.detectChanges();
const content = element.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '"log in"');
});
Testing Components with a dependency -testing the service
import {Component} from '@angular/core';
import {GreetingsService} from './geetings.service';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message: string = 'Hello';
constructor(private greetingsService: GreetingsService) { }
ngOnInit(): void {
this.greetingsService.getGreets()
.then(greets => this.message = greets[0]);
}
}
Testing Components with an async dependency
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
providers: [ GreetingsService ],
});
fixture = TestBed.createComponent(GreetComponent);
component = fixture.componentInstance;
greetingsService = fixture.debugElement.injector.get(GreetingsService);
spy = spyOn(greetingsService, 'getGreetings')
.and.returnValue(Promise.resolve(testGreetings));
debugElement = fixture.debugElement.query(By.css('h1'));
element = debugElement.nativeElement;
});
Testing Components with an async dependency
it('should not show greeting before OnInit', () => {
expect(element.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getGreetings not yet called');
});
it('should still not show greeting after component initialized', () => {
fixture.detectChanges();
expect(element.textContent).toBe('Hello', 'no greeting yet');
expect(spy.calls.any()).toBe(true, 'getGreetings called');
});
Testing Components with an async dependency
Sync test
it('should show message after getGreetings promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.textContent).toBe(testGreeting);
});
}));
it('should show message after getGreetings promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(element.textContent).toBe(testGreeting);
}));
Testing Components with an async dependency
Async test
Testing Components with an async dependency
Testing Components with inputs and outputs
<greet-message
[greeting]=greet
(onLiked)="incrementLikes($event)"
>
</greet-message>
@Component({
selector: 'greet-message',
template: `<h1>
{{message}} <button (click)=handleClick>LIKE</button>
</h1>`
})
export class GreetComponent {
@Input() message: string;
@Output() onLiked = new EventEmitter<string>();
handleClick() {
this.selected.emit(this.message);
}
}
Testing Components with inputs and outputs
Goal
Approaches
beforeEach(() => {
fixture = TestBed.createComponent(GreetingsComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.css('h1'));
expectedMessage = 'Wassuuup?!?';
component.message = expectedMessage;
fixture.detectChanges();
});
Testing Components with inputs and outputs - standalone
Test standalone
it('should raise selected event when clicked', () => {
let likedMessage: string;
comp.selected.subscribe((message: string) => likedMessage = message);
buttonElement.triggerEventHandler('click', null);
expect(likedMessage).toBe(expectedMessage);
});
it('should display greeting', () => {
expect(element.nativeElement.textContent).toContain(expectedMessage);
});
Testing Components with inputs and outputs - host component
Test in host component
@Component({
template: `
<greet-message
[greeting]=greet
(onLiked)="handleLike($event)"
>
</greet-message>
`
})
class TestHostComponent {
greet = 'Wassuuuup?!?';
handleLike(string: message) { /* do something with with the like */ }
}
Testing Components with inputs and outputs - host component
Test in host component
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ GreetComponent, TestHostComponent ],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
greetElement = fixture.debugElement.query(By.css('h1'));
fixture.detectChanges();
});
Testing Components that uses the router
@Component({
selector: 'greet-message',
template: `<h1>
{{message}}
<br />
<button (click)=handleClick>go to your page</button>
</h1>`
})
export class GreetComponent {
@Input() message: string;
@Output() onLiked = new EventEmitter<string>();
constructor(
private router: Router,
private userService: UserService) {
}
handleClick() {
let url = `/users/${this.userService.id}`;
this.router.navigateByUrl(url);
}
}
Testing Components that uses the router
class RouterStub {
navigateByUrl(url: string) { return url; }
}
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [GreetingComponent],
providers: [
{ provide: UserService, useClass: UserServiceStub },
{ provide: Router, useClass: RouterStub }
]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(GreetingComponent);
comp = fixture.componentInstance;
});
Testing Components that uses the router
it('should tell ROUTER to navigate when greet button is clicked',
inject([Router], (router: Router) => {
const spy = spyOn(router, 'navigateByUrl');
greetClick(component); // should trigger the click on the greeting button
const navArgs = spy.calls.first().args[0]; // arguments for call
// get actual service injected in component
userService = fixture.debugElement.injector.get(UserService);
const id = userService.getUser().id;
expect(navArgs).toBe('/users/' + id, 'should nav to User details ');
}));
Other advanced testing techniques
Isolated unit test advantages:
import { Injectable } from '@angular/core';
@Injectable()
export class Engine {
getHorsepower() {
return 150;
}
getName() {
return 'Basic engine';
}
}
Verifying Methods and Properties - isolated test
describe('Engine', () => {
let subject: Engine;
beforeEach(() => {
subject = new Engine();
});
it('should return it\'s horsepower', () => {
expect(subject.getHorsepower()).toEqual(150);
});
it('should return it\'s horsepower', () => {
expect(subject.getName()).toEqual('Basic engine');
});
});
Verifying Methods and Properties - isolated test
import { Injectable } from '@angular/core';
import { Engine } from './engine.service';
@Injectable()
export class Car {
constructor(private engine: Engine) {}
getName() {
return `Car with ${this.engine.getName()}(${this.engine.getHorsepower()} HP)`;
}
}
Using Dependency Injection
import { TestBed, inject } from '@angular/core/testing';
import { Engine } from './engine.service';
import { Car } from './car.service';
describe('Car', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [Engine, Car]
});
});
it('should display name with engine', inject([Car], (car: Car) => {
expect(car.getName()).toEqual('Car with Basic engine(150 HP)');
}));
});
Using Dependency Injection
...
beforeEach(() => {
TestBed.configureTestingModule({
providers: [Engine, Car]
});
spyOn(Engine.prototype, 'getHorsepower').and.returnValue(400);
spyOn(Engine.prototype, 'getName').and.returnValue('V8 engine');
});
...
it('should display name with engine', () => {
expect(subject.getName()).toEqual('Car with V8 engine(400 HP)');
});
Using Dependency Injection - mocking
@Injectable()
class V8Engine {
getHorsepower() {
return 400;
}
getName() {
return 'V8 engine';
}
}
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: Engine, useClass: V8Engine },
Car
]
});
});
Using Dependency Injection - stubbing