Angular Workshop
Testing
Contents
- Why test
- Testing tools
- Running a first test
- Testing components
- Unit testing services
Why test?
Testing Tools
JS testing tools
-
Test frameworks - where you write your test
- Jasmine, Mocha, Tape, Jest
-
Test environment - where your tests are executed
- browsers - Chrome, Firefox...
- headless browsers - JSDom, PhantomJS
-
Test runners - where you run your tests
- Karma
Jasmine
- A behavior-driven development framework for testing JavaScript code.
- Create hierarchical suites of test (describe)
- The tests are written as specifications (it)
- Expectations and Matchers (built-in and custom)
- Spies - a test double pattern
- Asynchronous operations support
Jasmine
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
Jasmine Matchers
- not
- toBe
- toEqual
- toMatch
- toBeDefined
- toBeUndefined
- toBeNull
- toBeTruthy
- toBeFalsy
- toContain
- toBeLessThan
- toBeGreaterThan
- toBeCloseTo
- toThrow
Jasmine
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';
};
}
Jasmine
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']);
Jasmine
Async support
describe("asynchronous specs",() => {
it("should take a long time",(done) => {
setTimeout(function() {
done();
}, 9000);
});
})
Karma
- JavaScript test runner that integrates with a browser environment
- Created by the AngularJS team
- Configuration file
- browser launchers
- test framework
- reporters
- preprocessors
Assignment
Setting up Karma with Jasmine and TypeScript
Testing Angular Components
Testing Components
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
Testing Components
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
Testing Components
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
Testing Components
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
Testing Components
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
});
fixture = TestBed.createComponent(GreetComponent);
Testing with Angular testing utilities - setup
TestBed
- Created in beforeEach() - called before every test
- An Angular testing module—an @NgModule class
- call configureTestingModule with a metadata object
Testing Components
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
});
fixture = TestBed.createComponent(GreetComponent);
Testing with Angular testing utilities - setup
TestBed
- Supports optional override methods
TestBed.overrideComponent(SomeComponent, {
set: {
template: '<div>Overridden template here</div>'
// ...
}
});
Testing Components
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()
- creates an instance of the component to test
- returns a component fixture
- access to the component instance
- access to the debug element
- closes current TestBed configurations
Testing Components
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 = '';
}
}
Testing Components
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
Testing Components
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
declarations: [ GreetComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Testing with Angular testing utilities - the tests
detectChanges()
- tells Angular when to perform change detection
- TestBed.createComponent() does not trigger change detection
- It is possible to set up automatic change detection
Testing Components
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
- compileComponents() - asynchronously compiles all the components configured in the testing module
- async() - arranges for the body of the beforeEach to run in a special async test zone that hides the mechanics of asynchronous execution
Testing Components
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
Testing Components
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
Testing Components
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
- always access service from injector, never directly - the injected service is a clone
Testing Components
// 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);
- The component injector
- The TestBed injector
it('stub object and injected UserService should not be the same', () => {
expect(userServiceStub === userService).toBe(false);
userServiceStub.isLoggedIn = false;
expect(userService.isLoggedIn).toBe(true);
});
Testing Components
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
- The component injector
Testing Components
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
Testing Components
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
- Inject real service (not a stub) and use a Spy for the called method - bypasses the actual method implementation
Testing Components
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
- The spy allows us to test that the method has been called
- Neither test can access the value in the returned (and resolved) promise (from the spy)
Testing Components
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
Testing Components with an async dependency
- the async() function - running the test in a special async test zone
- no access to promises called in component
- whenStable() - called when all async operations in the test complete (still need to detect changes)
- fakeAsync() - allows for a more linear coding style, no need for then()-s
- tick() - can only be called inside a fakeAsync, “waits” for all async operations to finish
- async and fake async - just sugar over jasmine.done()
Testing Components
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
Testing Components with inputs and outputs
Goal
- Test if inputs and outputs work correctly
Approaches
- test as standalone component
- test inside a container component
beforeEach(() => {
fixture = TestBed.createComponent(GreetingsComponent);
component = fixture.componentInstance;
element = fixture.debugElement.query(By.css('h1'));
expectedMessage = 'Wassuuup?!?';
component.message = expectedMessage;
fixture.detectChanges();
});
Testing Components
Testing Components with inputs and outputs - standalone
Test standalone
- Outputs - subscribe to EventEmitter, trigger click event
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);
});
- Inputs - set a value to the input property on the component object
it('should display greeting', () => {
expect(element.nativeElement.textContent).toContain(expectedMessage);
});
Testing Components
Testing Components with inputs and outputs - host component
Test in host component
- create an on the fly component to test the target 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
Testing Components with inputs and outputs - host component
Test in host component
- Declare both components in TestBed
- Create only the test 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
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
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;
});
- Just stub the used methods for the Router
Testing Components
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 ');
}));
- Router is injected using TestBed injector
- The UserService is actual service injected by the component
Testing Components
Other advanced testing techniques
- Test doubles for observables
- Override component providers
- Testing router outlet components
- Shallow render
- Testing directives
Testing Angular Services
Testing Services
Isolated unit test advantages:
- Import from the Angular test libraries.
- Configure a module.
- Prepare dependency injection providers.
- Call inject or async or fakeAsync.
Testing Services
import { Injectable } from '@angular/core';
@Injectable()
export class Engine {
getHorsepower() {
return 150;
}
getName() {
return 'Basic engine';
}
}
Verifying Methods and Properties - isolated test
Testing Services
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
Testing Services
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
Testing Services
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
Testing Services
...
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
Testing Services
@Injectable()
class V8Engine {
getHorsepower() {
return 400;
}
getName() {
return 'V8 engine';
}
}
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
{ provide: Engine, useClass: V8Engine },
Car
]
});
});
Using Dependency Injection - stubbing
Assignment
Angular Workshop - testing
By Andrei Antal
Angular Workshop - testing
- 1,382