19. Testing
1st Test (ng test || npm test):
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});
BannerComponent(Component Under Test):
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: `<h1>{{ title }}</h1>`
})
export class BannerComponent {
title = 'Test Tour of Technologies';
}
Test:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './app.banner.component';
describe('BannerComponent', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [BannerComponent] // declare test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // bannercomponent test instance
// query for title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
it('no title in DOM until manually call detectChanges', () => {
expect(el.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'My Polymer Princess, I love you.';
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
});
Let's view the results of ng test in Chromium and in our Terminal:
Test Component with External Template:
import { Component } from '@angular/core';
@Component({
selector: 'app-banner-external',
templateUrl: './app.banner-external.component.html',
styleUrls: ['./app.banner-external.component.css']
})
export class BannerExternalComponent {
title = 'Test Tour of Technologies';
}
External Template and CSS Files:
<h1>{{ title }}</h1>
h1 {
color: red;
font-size: 450%;
}
Test(async):
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerExternalComponent } from './app.banner-external.component';
describe('BannerExternalComponent', () => {
let comp: BannerExternalComponent;
let fixture: ComponentFixture<BannerExternalComponent>;
let de: DebugElement;
let el: HTMLElement;
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerExternalComponent ]
})
.compileComponents();
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerExternalComponent);
comp = fixture.componentInstance; // bannerexternalcomponent test instance
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
it('no title in DOM until manually call detectChanges', () => {
expect(el.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Hi baby!';
fixture.detectChanges();
expect(el.textContent).toContain('Hi baby!');
});
})
Test Component with Dependency
UserService:
import { Injectable } from '@angular/core';
@Injectable()
export class UserService {
isLoggedIn = true;
user = { name: 'Nils-Holger Nägele'};
}
WelcomeComponent:
import { Component, OnInit } from '@angular/core';
import { UserService } from './model/user.service';
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome"><i>{{ welcome }}</i></h3>'
})
export class WelcomeComponent implements OnInit {
welcome = '--- not initialized yet ---';
constructor(private userService: UserService) { }
ngOnInit() {
this.welcome = this.userService.isLoggedIn ? 'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
Test Spec:
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { UserService } from './model';
import { WelcomeComponent } from './welcome.component';
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let componentUserService: UserService; // actually injected service
let userService: UserService; // testbed injected service
let de: DebugElement; // debugelement with welcome message
let el: HTMLElement; // dom element with welcome message
let userServiceStub: {
isLoggedIn: boolean;
user: { name: string }
};
beforeEach(() => {
// stub userservice for test purposes
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User' }
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ], NO don't provide real service! provide test-double instead
providers: [{ provide: UserService, useValue: userServiceStub }]
});
fixture = TestBed.createComponent(WelcomeComponent);
comp = fixture.componentInstance;
// userservice actually injected into component
userService = fixture.debugElement.injector.get(UserService);
componentUserService = userService;
// userservice from root injector
userService = TestBed.get(UserService);
// get welcome element by css selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
it('should welcome the user', () => {
fixture.detectChanges();
const content = el.textContent;
expect(content).toContain('Welcome', '"Welcome ..."');
expect(content).toContain('Test User', 'expected name');
});
it('should welcome "Polymer Princess"', () => {
userService.user.name = 'Polymer Princess'; // welcome message hasn't been shown yet
fixture.detectChanges();
expect(el.textContent).toContain('Polymer Princess');
});
it('should request login if not logged in', () => {
userService.isLoggedIn = false; // welcome message hasn't been shown yet
fixture.detectChanges();
const content = el.textContent;
expect(content).not.toContain('Welcome', 'not welcomed');
expect(content).toMatch(/log in/i, '"log in"');
});
it('should inject the component\'s user service instance',
inject([UserService], (service: UserService) => {
expect(service).toBe(componentUserService);
}));
it('TestBed and Component UserService should be the same', () => {
expect(userService === componentUserService).toBe(true);
});
it('stub object and injected User Service should not be the same', () => {
expect(userServiceStub === userService).toBe(false);
// changing the stub object has no effect on the injected service
userServiceStub.isLoggedIn = false;
expect(userService.isLoggedIn).toBe(true);
});
});
Testing Component with Async Service
LinuxService:
import { Injectable } from '@angular/core';
const quotes = [
'We are using Linux daily to UP our productivity - so UP yours!',
'The Linux philosophy is "laugh in the face of danger". Oops. Wrong one. "Do it yourself".',
'Linux is the choice of a GNU generation.',
'Only wimps use tape backup: _real_men just upload the important stuff on ftp, and let the rest of the world mirror it.',
'Be warned that typing killall name may not have the desired effect on non-Linux systems, especially when done by a privileged user.',
'A multithreaded file system is only a performance hack.',
'Anyone can build a fast processor, the trick is to build a fast system.',
'If Bill Gates is the Devil then Linus Torvalds must be the Messiah.',
'All the best people in Life seem to like Linux.',
'I trust my family jewels only to Linux.',
'Making Linux GPL\'d was definitely the best thing I ever did.'
];
@Injectable()
export class LinuxService {
private next = 0;
getQuote(): Promise<string> {
return new Promise(resolve => {
setTimeout(() => resolve(this.nextQuote()), 500);
});
}
private nextQuote() {
if (this.next === quotes.length) { this.next = 0; }
return quotes[ this.next++ ];
}
}
LinuxComponent:
import { Component, OnInit } from '@angular/core';
import { LinuxService } from './linux.service';
@Component({
selector: 'app-linux-quote',
template: '<p class="linux"><i>{{ quote }}</i></p>'
})
export class LinuxComponent implements OnInit {
quote = '...';
constructor(private linuxService: LinuxService) { }
ngOnInit() {
this.linuxService.getQuote().then(quote => this.quote = quote);
}
}
Test Specs:
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { LinuxService } from './linux.service';
import { LinuxComponent } from './linux.component';
describe('LinuxComponent', () => {
let comp: LinuxComponent;
let fixture: ComponentFixture<LinuxComponent>;
let spy: jasmine.Spy;
let de: DebugElement;
let el: HTMLElement;
let linuxService: LinuxService; // actually injected service
const myFavoriteQuote = 'Software is like sex, it\'s better when it\'s free.';
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ LinuxComponent ],
providers: [ LinuxService ]
});
fixture = TestBed.createComponent(LinuxComponent);
comp = fixture.componentInstance;
// LinuxService actually injected into component
linuxService = fixture.debugElement.injector.get(LinuxService);
// setup spy on getquote method
spy = spyOn(linuxService, 'getQuote').and.returnValue(Promise.resolve(myFavoriteQuote));
// get linux quote element by CSS selector(e.g., by class name)
de = fixture.debugElement.query(By.css('.linux'));
el = de.nativeElement;
});
it('should not show a quote before OnInit', () => {
expect(el.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
expect(el.textContent).toBe('...', 'no quote yet');
expect(spy.calls.any()).toBe(true, 'getQuote called');
});
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(myFavoriteQuote);
});
}));
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(el.textContent).toBe(myFavoriteQuote);
}));
it('should show quote after getQuote promise (done)', (done: any) => {
fixture.detectChanges();
// get spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(myFavoriteQuote);
done();
});
});
});
Let's view the results of ng test in our Chromium Browser:
DashboardTechnologyComponent:
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Technology } from '../model';
@Component({
selector: 'app-dashboard-technology',
template: `
<div (click)="click()" class="technology">
{{ technology.name | uppercase }}
</div>
`,
styleUrls: ['./dashboard-technology.component.css']
})
export class DashboardTechnologyComponent {
@Input() technology: Technology;
@Output() selected = new EventEmitter<Technology>();
click() { this.selected.emit(this.technology); }
}
Test Specs:
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, click } from '../../testing';
import { Technology } from '../model/technology';
import { DashboardTechnologyComponent } from './dashboard-technology.component';
beforeEach( addMatchers );
describe('DashboardTechnologyComponent when tested directly', () => {
let comp: DashboardTechnologyComponent;
let expectedTechnology: Technology;
let fixture: ComponentFixture<DashboardTechnologyComponent>;
let technologyEl: DebugElement;
// async beforeeach
beforeEach( async() => {
TestBed.configureTestingModule({
declarations: [ DashboardTechnologyComponent ]
})
.compileComponents(); // compile template and css
});
// synchronous beforeeach
beforeEach(() => {
fixture = TestBed.createComponent(DashboardTechnologyComponent);
comp = fixture.componentInstance;
technologyEl = fixture.debugElement.query(By.css('.technology')); // find technology element
// pretend it was wired to something that supplies a technology
expectedTechnology = new Technology(42, 'Test Future Killer Technology');
comp.technology = expectedTechnology;
fixture.detectChanges(); // trigger initial data binding
});
it('should display technology name', () => {
const expectedPipedName = expectedTechnology.name.toUpperCase();
expect(technologyEl.nativeElement.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
let selectedTechnology: Technology;
comp.selected.subscribe((technology: Technology) => selectedTechnology = technology);
technologyEl.triggerEventHandler('click', null);
expect(selectedTechnology).toBe(expectedTechnology);
});
it('should raise selected event when clicked (click helper)', () => {
let selectedTechnology: Technology;
comp.selected.subscribe((technology: Technology) => selectedTechnology = technology);
click(technologyEl); // triggereventhandler helper
expect(selectedTechnology).toBe(expectedTechnology);
});
});
describe('DashboardTechnologyComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let technologyEl: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardTechnologyComponent, TestHostComponent ]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
technologyEl = fixture.debugElement.query(By.css('.technology'));
fixture.detectChanges();
});
it('should display technology name', () => {
const expectedPipedName = testHost.technology.name.toUpperCase();
expect(technologyEl.nativeElement.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
click(technologyEl);
expect(testHost.selectedTechnology).toBe(testHost.technology);
});
});
////// Test Host Component //////
import { Component } from '@angular/core';
@Component({
template: `<app-dashboard-technology [technology]="technology" (selected)="onSelected($event)">
</app-dashboard-technology>`
})
class TestHostComponent {
technology = new Technology(42, 'Test Future Killer Technology');
selectedTechnology: Technology;
onSelected(technology: Technology) { this.selectedTechnology = technology; }
}
Let's view the test Results in Chromium:
Let's have a look at the running App in our Chromium Browser:
Test Routed Component:
ABC.XYZ 123