KEMBAR78
Angular Unit Testing from the Trenches | PDF
http://digitaldrummerj.me
Justin James
Web Developer
Professional Speaker
digitaldrummerj.me
@digitaldrummerj
http://digitaldrummerj.me
unit tests are proof that my
code does what I think it does
http://digitaldrummerj.me
Document FunctionalityPinpoint Bugs
Check RegressionConfirm Functionality
Unit Test Will Make You Faster
http://digitaldrummerj.me
Checks Single
Assumption
Single Unit
Of Work
Automated
Code
Tenents
http://digitaldrummerj.me
We only get these advantages when we are comfortable writing good
tests
Disclaimer
http://digitaldrummerj.me
RunnableDependableMaintainable
Good Unit Tests
http://digitaldrummerj.me
Well-namedEasy To Write
Easy To ReadNot Tricky
Maintainable
http://digitaldrummerj.me
Tests Right ThingsContinued Relevance
IsolatedConsistent Results
Dependable
http://digitaldrummerj.me
Failures Point To
The Problem
Repeatable
Fast
Runnable
Single Click
http://digitaldrummerj.me
Unit testing
will not fix
bad practices
Don't do the right thing
for the wrong reason
Know the Goals
http://digitaldrummerj.me
"Don't let the fear that testing can't catch
all bugs stop you from writing the tests
that will catch most bugs"
Martin Fowler
Refactoring
http://digitaldrummerj.me
You need a strong grasp of the
basic patterns in order to test
an Angular application
http://digitaldrummerj.me
ServicesRouting
ComponentsModules
Angular Main Building Blocks
13
http://digitaldrummerj.me
http://nodejs.org
Node JS (6.9+)
npm install -g @angular/cli
NPM Global Packages
Angular CLI
Setup
14
http://digitaldrummerj.me
ng new AppName --style scss --routing
Create New Project
ng generate [TYPE] [NAME]
Generate
npm start
Start Application
npm test
Unit Test
Creating
Applications
15
http://digitaldrummerj.me
Test Runner Test Framework Testing Utilities
Karma
Tools and Technology
Jasmine Angular
http://digitaldrummerj.me
http://digitaldrummerj.me
http://digitaldrummerj.me
Debugging with Karma
Use the Chrome Developer Console to debug tests
Use console.log to observe data and events
describe('First spec', () => {
it('should pass', () => {
expect(true).toBeTruthy();
});
});
Simple Passing Test
describe('First spec', () => {
it('should pass', () => {
expect(true).toBeTruthy();
});
});
Simple Passing Test
describe('First spec', () => {
it('should pass', () => {
expect(true).toBeTruthy();
});
});
Simple Passing Test
describe(Second spec', () => {
it('should fail', () => {
expect(false).toBeTruthy();
});
});
Simple Failing Test
http://digitaldrummerj.me
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string;
constructor() { }
ngOnInit() { }
}
Component
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string = "World";
constructor() { }
ngOnInit() { }
}
Component
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string = "World";
constructor() { }
ngOnInit() { }
}
Component
http://digitaldrummerj.me
1. Configure Module
http://digitaldrummerj.me
TestBed
Creates an Angular testing module
Configure with TestBed.configureTestingModule
Use BeforeEach to reset test module before each spec
fixture
import { async, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SimpleComponent]
})
.compileComponents()
.then(() => {
});
}));
}
fixture
import { async, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SimpleComponent]
})
.compileComponents()
.then(() => {
});
}));
}
fixture
import { async, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SimpleComponent]
})
.compileComponents()
.then(() => {
});
}));
}
fixture
import { async, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SimpleComponent]
})
.compileComponents()
.then(() => {
});
}));
}
http://digitaldrummerj.me
1. Configure Module
2. Create Fixture
http://digitaldrummerj.me
TestBed.createComponent
Creates an instance of the component under test
Returns a component test fixture
Closes the TestBed from further configuration
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SimpleComponent ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(SimpleComponent);
});
}));
}
The Fixture
http://digitaldrummerj.me
1. Configure Module
2. Create Fixture
3. Get Component Instance
http://digitaldrummerj.me
ComponentFixture
Handle to the test environment
fixture.componentInstance to access to component
The Component Instance
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ TemplateComponent ]
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(TemplateComponent);
component = fixture.componentInstance;
});
}));
}
The Component Instance
it('sets the `subject` class member', () => {
expect(component.subject).toBe('world');
});
http://digitaldrummerj.me
ComponentFixture.detectChanges
Triggers change detection
Not triggered by TestBed.createComponent
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent)
.then(() => {
component = fixture.componentInstance;
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
}));
}
The Debug Element
http://digitaldrummerj.me
DebugElement
DebugElement to access the component's DOM
DebugElement.query to query the DOM
By.css to query DOM using CSS selectors
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SimpleComponent } from './simple.component';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
describe('SimpleComponent', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
let element: DebugElement;
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ SimpleComponent ]
})
.createComponent(SimpleComponent)
.then(() => {
component = fixture.componentInstance;
element = fixture.debugElement;
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
}));
}
The Debug Element
it('greets the subject', () => {
const h1 = element.query(By.css('h1'));
expect(h1.nativeElement.innerText).toBe('Hello world!');
});
The Debug Element
it('greets the subject', () => {
const h1 = element.query(By.css('h1'));
expect(h1.nativeElement.innerText).toBe('Hello world!');
});
The Debug Element
it('greets the subject', () => {
const h1 = element.query(By.css('h1'));
expect(h1.nativeElement.innerText).toBe('Hello world!');
});
The Debug Element
http://digitaldrummerj.me
Service
export class GreetingService {
subject: string = 'world';
}
import { GreetingService } from '@./services/greeting.service';
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string = this.service.subject;
constructor(private service: GreetingService) { }
ngOnInit() { }
}
Component
import { GreetingService } from '@./services/greeting.service';
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string = this.service.subject;
constructor(private service: GreetingService) { }
ngOnInit() { }
}
Component
import { GreetingService } from '@./services/greeting.service';
@Component({
selector: 'app-service',
templateUrl: './simple.component.html',
styleUrls: ['./simple.component.scss']
})
export class SimpleComponent implements OnInit {
subject: string = this.service.subject;
constructor(private service: GreetingService) { }
ngOnInit() { }
}
Component
http://digitaldrummerj.me
Don't Use Real Services
http://digitaldrummerj.me
Don't Use Real Services
Use Test Double
http://digitaldrummerj.me
Don't Use Real Services
Use Test Double
Use A Stub
http://digitaldrummerj.me
Don't Use Real Services
Use Test Double
Use A Stub
Use A Spy
http://digitaldrummerj.me
Override Provider With
useValue or useClass
Local Members
describe('Simple Component with Service', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
let element: DebugElement;
let greetingServiceStub: GreetingServiceStub
let greetingService: GreetingService;
});
Local Members
describe('Simple Component with Service', () => {
let component: SimpleComponent;
let fixture: ComponentFixture<SimpleComponent>;
let element: DebugElement;
let greetingServiceStub: GreetingServiceStub
let greetingService: GreetingService;
});
export class GreetingServiceStub {
service = "Test World!"
}
fixture = TestBed.configureTestingModule({
declarations: [SimpleComponent ],
providers: [{ provide: GreetingService, useClass: GreetingServiceStub }]
})
.createComponent(SimpleComponent)
.then(() => {
fixture = TestBed.createComponent(SimpleComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
greetingService = de.injector.get(GreetingService);
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
Test Double
export class GreetingServiceStub {
service = "Test World!"
}
fixture = TestBed.configureTestingModule({
declarations: [SimpleComponent ],
providers: [{ provide: GreetingService, useClass: GreetingServiceStub }]
})
.createComponent(SimpleComponent)
.then(() => {
fixture = TestBed.createComponent(SimpleComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
greetingService = de.injector.get(GreetingService);
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
Test Double
http://digitaldrummerj.me
Get Service Reference With
debugElement.injector
export class GreetingServiceStub {
service = "Test World!"
}
fixture = TestBed.configureTestingModule({
declarations: [SimpleComponent ],
providers: [{ provide: GreetingService, useValue: greetingServiceStub }]
})
.createComponent(SimpleComponent)
.then(() => {
fixture = TestBed.createComponent(SimpleComponent);
component = fixture.componentInstance;
element = fixture.debugElement;
greetingService = de.injector.get(GreetingService);
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
});
Test Double
Actual Test
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
Actual Test
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
Actual Test
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
Actual Test
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
Actual Test
it('updates component subject when service subject is changed', () => {
greetingService.subject.name = 'cosmos';
fixture.detectChanges();
expect(component.subject.name).toBe('cosmos');
const h1 = de.query(By.css('h1')).nativeElement;
expect(h1.innerText).toBe('Hello cosmos!');
});
http://digitaldrummerj.me
Service
@Injectable()
export class GreetingService {
subject: string = 'world' ;
getGreeting() { return Promise.resolve('Hello'); }
getSubject() { return Promise.resolve(this.subject.name); }
getPunctuation() { return Promise.resolve('!'); }
}
Service
@Injectable()
export class GreetingService {
subject: string = 'world' ;
getGreeting() { return Promise.resolve('Hello'); }
getSubject() { return Promise.resolve(this.subject.name); }
getPunctuation() { return Promise.resolve('!'); }
}
Component
export class SimpleComponent implements OnInit {
subject: string;
greeting: string;
punctuation: string;
constructor(private service: GreetingService) { }
ngOnInit() {
this.service.getGreeting()
.then(res => this.greeting = res);
this.service.getSubject()
.then(res => this.subject = res);
this.service.getPunctuation()
.then(res => this.punctuation = res);
}
}
Component
export class SimpleComponent implements OnInit {
subject: string;
greeting: string;
punctuation: string;
constructor(private service: GreetingService) { }
ngOnInit() {
this.service.getGreeting()
.then(res => this.greeting = res);
this.service.getSubject()
.then(res => this.subject = res);
this.service.getPunctuation()
.then(res => this.punctuation = res);
}
}
Setup
beforeEach(async(() => {
fixture = TestBed.configureTestingModule({
declarations: [SimpleComponent ],
providers: [ GreetingService ]
})
.createComponent(SimpleComponent)
.then(() => {
component = fixture.componentInstance;
element = fixture.debugElement;
greetingService = de.injector.get(GreetingService);
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
}));
Setup
beforeEach(async(() => {
fixture = TestBed.configureTestingModule({
declarations: [SimpleComponent ],
providers: [ GreetingService ]
})
.createComponent(SimpleComponent)
.then(() => {
component = fixture.componentInstance;
element = fixture.debugElement;
greetingService = de.injector.get(GreetingService);
return fixture.whenStable().then(() => {
fixture.detectChanges();
});
});
}));
Spy with fixture.whenStable
it('gets `greeting` after promise (async)', async(() => {
spyOn(greetingService, 'getGreeting')
.and.returnValue(Promise.resolve('Greetings'));
expect(component.greeting).toBe('Greetings');
});
}));
Spy with fixture.whenStable
it('gets `greeting` after promise (async)', async(() => {
spyOn(greetingService, 'getGreeting')
.and.returnValue(Promise.resolve('Greetings'));
expect(component.greeting).toBe('Greetings');
});
}));
Spy with fixture.whenStable
it('gets `greeting` after promise (async)', async(() => {
spyOn(greetingService, 'getGreeting')
.and.returnValue(Promise.resolve('Greetings'));
expect(component.greeting).toBe('Greetings');
});
}));
Spy with fixture.whenStable
it('gets `greeting` after promise (async)', async(() => {
spyOn(greetingService, 'getGreeting')
.and.returnValue(Promise.resolve('Greetings'));
expect(component.greeting).toBe('Greetings');
});
}));
http://digitaldrummerj.me
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
Spy with tick
it('gets `subject` after promise (fakeAsync)', fakeAsync(() => {
spyOn(greetingService, 'getSubject')
.and.returnValue(Promise.resolve('universe'));
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(component.subject).toBe('universe');
}));
http://digitaldrummerj.me
http://digitaldrummerj.me
Component with Inputs and Outputs
The goal is to ensure that binding works as expected
Test input: simply update value and ensure it renders
Test output: trigger triggerEventHandler and subscribe to
the EventEmitter
@Component({
selector: 'app-input-output',
template: `
<h1>Hello {{subject}}!</h1>
<button (click)="depart()">We Out</button>
`
})
export class InputOutputComponent {
@Input('subject') subject: string;
@Output('leave') leave: EventEmitter<string> = new EventEmitter();
depart() {
this.leave.emit(`Later ${this.subject}!`);
}
}
Component
@Component({
selector: 'app-input-output',
template: `
<h1>Hello {{subject}}!</h1>
<button (click)="depart()">We Out</button>
`
})
export class InputOutputComponent {
@Input('subject') subject: string;
@Output('leave') leave: EventEmitter<string> = new EventEmitter();
depart() {
this.leave.emit(`Later ${this.subject}!`);
}
}
Component
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ InputOutputComponent ]
})
.createComponent(InputOutputComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
button = de.query(By.css('button'));
component.subject = 'galaxy';
fixture.detectChanges();
});
Setup
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ InputOutputComponent ]
})
.createComponent(InputOutputComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
button = de.query(By.css('button'));
component.subject = 'galaxy';
fixture.detectChanges();
});
Setup
beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ InputOutputComponent ]
})
.createComponent(InputOutputComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
button = de.query(By.css('button'));
component.subject = 'galaxy';
fixture.detectChanges();
});
Setup
it('has `subject` as an @Input', () => {
expect(component.subject).toBe('galaxy');
});
Input
it('says goodbye to the `subject`', () => {
let farewell;
component.leave.subscribe(event => farewell = event);
button.triggerEventHandler('click', { button: 0 });
expect(farewell).toBe('Later galaxy!');
});
Output
it('says goodbye to the `subject`', () => {
let farewell;
component.leave.subscribe(event => farewell = event);
button.triggerEventHandler('click', { button: 0 });
expect(farewell).toBe('Later galaxy!');
});
Output
it('says goodbye to the `subject`', () => {
let farewell;
component.leave.subscribe(event => farewell = event);
button.triggerEventHandler('click', { button: 0 });
expect(farewell).toBe('Later galaxy!');
});
Output
http://digitaldrummerj.me
http://digitaldrummerj.me
Routed Component
Avoid router complexity.
Use RouterTestingModule
Test that component navigates to proper route
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{path: '', redirectTo: '/items', pathMatch: 'full' },
{path: 'items', component: ItemsComponent},
{path: 'routed/:subject', component: RoutedComponent},
{path: 'widgets', component: WidgetsComponent},
{path: '**', redirectTo: '/items', pathMatch: 'full'}
]);
],
declarations: [
RoutedComponent
]
})
.createComponent(RoutedComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
});
Routes
import { RouterTestingModule } from '@angular/router/testing';
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{path: '', redirectTo: '/items', pathMatch: 'full' },
{path: 'items', component: ItemsComponent},
{path: 'routed/:subject', component: RoutedComponent},
{path: 'widgets', component: WidgetsComponent},
{path: '**', redirectTo: '/items', pathMatch: 'full'}
]);
],
declarations: [
RoutedComponent
]
})
.createComponent(RoutedComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
}); Routes
import { RouterTestingModule } from '@angular/router/testing';
beforeEach(() => {
fixture = TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{path: '', redirectTo: '/items', pathMatch: 'full' },
{path: 'items', component: ItemsComponent},
{path: 'routed/:subject', component: RoutedComponent},
{path: 'widgets', component: WidgetsComponent},
{path: '**', redirectTo: '/items', pathMatch: 'full'}
]);
],
declarations: [
RoutedComponent
]
})
.createComponent(RoutedComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
fixture.detectChanges();
}); Routes
RouterLink
import { RouterLinkWithHref } from '@angular/router';
import { fakeAsync } from '@angular/core/testing';
it('should have 1 routerLink in template', fakeAsync(() => {
allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
expect(allLinks.length).toBe(1, 'should have 1 link');
}));
RouterLink
import { RouterLinkWithHref } from '@angular/router';
import { fakeAsync } from '@angular/core/testing';
it('should have 1 routerLink in template', fakeAsync(() => {
allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
expect(allLinks.length).toBe(1, 'should have 1 link');
}));
RouterLink
import { RouterLinkWithHref } from '@angular/router';
import { fakeAsync } from '@angular/core/testing';
it('should have 1 routerLink in template', fakeAsync(() => {
allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
expect(allLinks.length).toBe(1, 'should have 1 link');
}));
RouterLink
import { RouterLinkWithHref } from '@angular/router';
import { fakeAsync } from '@angular/core/testing';
it('should have 1 routerLink in template', fakeAsync(() => {
allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
expect(allLinks.length).toBe(1, 'should have 1 link');
}));
RouterLink
import { RouterLinkWithHref } from '@angular/router';
import { fakeAsync } from '@angular/core/testing';
it('should have 1 routerLink in template', fakeAsync(() => {
allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
expect(allLinks.length).toBe(1, 'should have 1 link');
}));
Location Check
import { fakeAsync } from '@angular/core/testing';
it('all items takes me home', fakeAsync(() => {
const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
const linkDes = allLinkDes[0];
expect(location.path()).toBe('', 'link should not have navigated yet');
linkDes.triggerEventHandler('click', { button: 0 });
tick();
expect(location.path()).toBe('/items');
}));
Location Check
import { fakeAsync } from '@angular/core/testing';
it('all items takes me home', fakeAsync(() => {
const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
const linkDes = allLinkDes[0];
expect(location.path()).toBe('', 'link should not have navigated yet');
linkDes.triggerEventHandler('click', { button: 0 });
tick();
expect(location.path()).toBe('/items');
}));
Location Check
import { fakeAsync } from '@angular/core/testing';
it('all items takes me home', fakeAsync(() => {
const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
const linkDes = allLinkDes[0];
expect(location.path()).toBe('', 'link should not have navigated yet');
linkDes.triggerEventHandler('click', { button: 0 });
tick();
expect(location.path()).toBe('/items');
}));
Location Check
import { fakeAsync } from '@angular/core/testing';
it('all items takes me home', fakeAsync(() => {
const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
const linkDes = allLinkDes[0];
expect(location.path()).toBe('', 'link should not have navigated yet');
linkDes.triggerEventHandler('click', { button: 0 });
tick();
expect(location.path()).toBe('/items');
}));
Location Check
import { fakeAsync } from '@angular/core/testing';
it('all items takes me home', fakeAsync(() => {
const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
const linkDes = allLinkDes[0];
expect(location.path()).toBe('', 'link should not have navigated yet');
linkDes.triggerEventHandler('click', { button: 0 });
tick();
expect(location.path()).toBe('/items');
}));
http://digitaldrummerj.me
Document FunctionalityPinpoint Bugs
Check RegressionConfirm Functionality
Unit Test Will Make You Faster
http://digitaldrummerj.me
Checks Single
Assumption
Single Unit
Of Work
Automated
Code
Tenents
http://digitaldrummerj.me
RunnableDependableMaintainable
Good Unit Tests
http://digitaldrummerj.me
Resources
Angular Unit Testing Guide:
angular.io/guide/testing
Code Repository:
github.com/digitaldrummerj/angular-tutorial-code/tree/chapter-
unit-test
Slides:
slideshare.net/digitaldrummerj/angular-unit-testing-from-the-
trenches
http://digitaldrummerj.me
digitaldrummerj.me
@digitaldrummerj
http://digitaldrummerj.me
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches
Angular Unit Testing from the Trenches

Angular Unit Testing from the Trenches

  • 1.
    http://digitaldrummerj.me Justin James Web Developer ProfessionalSpeaker digitaldrummerj.me @digitaldrummerj
  • 2.
    http://digitaldrummerj.me unit tests areproof that my code does what I think it does
  • 3.
    http://digitaldrummerj.me Document FunctionalityPinpoint Bugs CheckRegressionConfirm Functionality Unit Test Will Make You Faster
  • 4.
  • 5.
    http://digitaldrummerj.me We only getthese advantages when we are comfortable writing good tests Disclaimer
  • 6.
  • 7.
  • 8.
    http://digitaldrummerj.me Tests Right ThingsContinuedRelevance IsolatedConsistent Results Dependable
  • 9.
    http://digitaldrummerj.me Failures Point To TheProblem Repeatable Fast Runnable Single Click
  • 10.
    http://digitaldrummerj.me Unit testing will notfix bad practices Don't do the right thing for the wrong reason Know the Goals
  • 11.
    http://digitaldrummerj.me "Don't let thefear that testing can't catch all bugs stop you from writing the tests that will catch most bugs" Martin Fowler Refactoring
  • 12.
    http://digitaldrummerj.me You need astrong grasp of the basic patterns in order to test an Angular application
  • 13.
  • 14.
    http://digitaldrummerj.me http://nodejs.org Node JS (6.9+) npminstall -g @angular/cli NPM Global Packages Angular CLI Setup 14
  • 15.
    http://digitaldrummerj.me ng new AppName--style scss --routing Create New Project ng generate [TYPE] [NAME] Generate npm start Start Application npm test Unit Test Creating Applications 15
  • 16.
    http://digitaldrummerj.me Test Runner TestFramework Testing Utilities Karma Tools and Technology Jasmine Angular
  • 17.
  • 18.
  • 19.
    http://digitaldrummerj.me Debugging with Karma Usethe Chrome Developer Console to debug tests Use console.log to observe data and events
  • 20.
    describe('First spec', ()=> { it('should pass', () => { expect(true).toBeTruthy(); }); }); Simple Passing Test
  • 21.
    describe('First spec', ()=> { it('should pass', () => { expect(true).toBeTruthy(); }); }); Simple Passing Test
  • 22.
    describe('First spec', ()=> { it('should pass', () => { expect(true).toBeTruthy(); }); }); Simple Passing Test
  • 23.
    describe(Second spec', ()=> { it('should fail', () => { expect(false).toBeTruthy(); }); }); Simple Failing Test
  • 24.
  • 25.
    @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls:['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string; constructor() { } ngOnInit() { } } Component
  • 26.
    @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls:['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string = "World"; constructor() { } ngOnInit() { } } Component
  • 27.
    @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls:['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string = "World"; constructor() { } ngOnInit() { } } Component
  • 28.
  • 29.
    http://digitaldrummerj.me TestBed Creates an Angulartesting module Configure with TestBed.configureTestingModule Use BeforeEach to reset test module before each spec
  • 30.
    fixture import { async,TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SimpleComponent] }) .compileComponents() .then(() => { }); })); }
  • 31.
    fixture import { async,TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SimpleComponent] }) .compileComponents() .then(() => { }); })); }
  • 32.
    fixture import { async,TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SimpleComponent] }) .compileComponents() .then(() => { }); })); }
  • 33.
    fixture import { async,TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SimpleComponent] }) .compileComponents() .then(() => { }); })); }
  • 34.
  • 35.
    http://digitaldrummerj.me TestBed.createComponent Creates an instanceof the component under test Returns a component test fixture Closes the TestBed from further configuration
  • 36.
    import { async,ComponentFixture, TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let fixture: ComponentFixture<SimpleComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [SimpleComponent ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(SimpleComponent); }); })); } The Fixture
  • 37.
    http://digitaldrummerj.me 1. Configure Module 2.Create Fixture 3. Get Component Instance
  • 38.
    http://digitaldrummerj.me ComponentFixture Handle to thetest environment fixture.componentInstance to access to component
  • 39.
    The Component Instance import{ async, ComponentFixture, TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ TemplateComponent ] }) .compileComponents() .then(() => { fixture = TestBed.createComponent(TemplateComponent); component = fixture.componentInstance; }); })); }
  • 40.
    The Component Instance it('setsthe `subject` class member', () => { expect(component.subject).toBe('world'); });
  • 41.
  • 42.
    import { async,ComponentFixture, TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent) .then(() => { component = fixture.componentInstance; return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); })); } The Debug Element
  • 43.
    http://digitaldrummerj.me DebugElement DebugElement to accessthe component's DOM DebugElement.query to query the DOM By.css to query DOM using CSS selectors
  • 44.
    import { async,ComponentFixture, TestBed } from '@angular/core/testing'; import { SimpleComponent } from './simple.component'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; describe('SimpleComponent', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let element: DebugElement; beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [ SimpleComponent ] }) .createComponent(SimpleComponent) .then(() => { component = fixture.componentInstance; element = fixture.debugElement; return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); })); } The Debug Element
  • 45.
    it('greets the subject',() => { const h1 = element.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello world!'); }); The Debug Element
  • 46.
    it('greets the subject',() => { const h1 = element.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello world!'); }); The Debug Element
  • 47.
    it('greets the subject',() => { const h1 = element.query(By.css('h1')); expect(h1.nativeElement.innerText).toBe('Hello world!'); }); The Debug Element
  • 48.
  • 49.
    Service export class GreetingService{ subject: string = 'world'; }
  • 50.
    import { GreetingService} from '@./services/greeting.service'; @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls: ['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string = this.service.subject; constructor(private service: GreetingService) { } ngOnInit() { } } Component
  • 51.
    import { GreetingService} from '@./services/greeting.service'; @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls: ['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string = this.service.subject; constructor(private service: GreetingService) { } ngOnInit() { } } Component
  • 52.
    import { GreetingService} from '@./services/greeting.service'; @Component({ selector: 'app-service', templateUrl: './simple.component.html', styleUrls: ['./simple.component.scss'] }) export class SimpleComponent implements OnInit { subject: string = this.service.subject; constructor(private service: GreetingService) { } ngOnInit() { } } Component
  • 53.
  • 54.
  • 55.
    http://digitaldrummerj.me Don't Use RealServices Use Test Double Use A Stub
  • 56.
    http://digitaldrummerj.me Don't Use RealServices Use Test Double Use A Stub Use A Spy
  • 57.
  • 58.
    Local Members describe('Simple Componentwith Service', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let element: DebugElement; let greetingServiceStub: GreetingServiceStub let greetingService: GreetingService; });
  • 59.
    Local Members describe('Simple Componentwith Service', () => { let component: SimpleComponent; let fixture: ComponentFixture<SimpleComponent>; let element: DebugElement; let greetingServiceStub: GreetingServiceStub let greetingService: GreetingService; });
  • 60.
    export class GreetingServiceStub{ service = "Test World!" } fixture = TestBed.configureTestingModule({ declarations: [SimpleComponent ], providers: [{ provide: GreetingService, useClass: GreetingServiceStub }] }) .createComponent(SimpleComponent) .then(() => { fixture = TestBed.createComponent(SimpleComponent); component = fixture.componentInstance; element = fixture.debugElement; greetingService = de.injector.get(GreetingService); return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); Test Double
  • 61.
    export class GreetingServiceStub{ service = "Test World!" } fixture = TestBed.configureTestingModule({ declarations: [SimpleComponent ], providers: [{ provide: GreetingService, useClass: GreetingServiceStub }] }) .createComponent(SimpleComponent) .then(() => { fixture = TestBed.createComponent(SimpleComponent); component = fixture.componentInstance; element = fixture.debugElement; greetingService = de.injector.get(GreetingService); return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); Test Double
  • 62.
  • 63.
    export class GreetingServiceStub{ service = "Test World!" } fixture = TestBed.configureTestingModule({ declarations: [SimpleComponent ], providers: [{ provide: GreetingService, useValue: greetingServiceStub }] }) .createComponent(SimpleComponent) .then(() => { fixture = TestBed.createComponent(SimpleComponent); component = fixture.componentInstance; element = fixture.debugElement; greetingService = de.injector.get(GreetingService); return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); }); Test Double
  • 64.
    Actual Test it('updates componentsubject when service subject is changed', () => { greetingService.subject.name = 'cosmos'; fixture.detectChanges(); expect(component.subject.name).toBe('cosmos'); const h1 = de.query(By.css('h1')).nativeElement; expect(h1.innerText).toBe('Hello cosmos!'); });
  • 65.
    Actual Test it('updates componentsubject when service subject is changed', () => { greetingService.subject.name = 'cosmos'; fixture.detectChanges(); expect(component.subject.name).toBe('cosmos'); const h1 = de.query(By.css('h1')).nativeElement; expect(h1.innerText).toBe('Hello cosmos!'); });
  • 66.
    Actual Test it('updates componentsubject when service subject is changed', () => { greetingService.subject.name = 'cosmos'; fixture.detectChanges(); expect(component.subject.name).toBe('cosmos'); const h1 = de.query(By.css('h1')).nativeElement; expect(h1.innerText).toBe('Hello cosmos!'); });
  • 67.
    Actual Test it('updates componentsubject when service subject is changed', () => { greetingService.subject.name = 'cosmos'; fixture.detectChanges(); expect(component.subject.name).toBe('cosmos'); const h1 = de.query(By.css('h1')).nativeElement; expect(h1.innerText).toBe('Hello cosmos!'); });
  • 68.
    Actual Test it('updates componentsubject when service subject is changed', () => { greetingService.subject.name = 'cosmos'; fixture.detectChanges(); expect(component.subject.name).toBe('cosmos'); const h1 = de.query(By.css('h1')).nativeElement; expect(h1.innerText).toBe('Hello cosmos!'); });
  • 69.
  • 70.
    Service @Injectable() export class GreetingService{ subject: string = 'world' ; getGreeting() { return Promise.resolve('Hello'); } getSubject() { return Promise.resolve(this.subject.name); } getPunctuation() { return Promise.resolve('!'); } }
  • 71.
    Service @Injectable() export class GreetingService{ subject: string = 'world' ; getGreeting() { return Promise.resolve('Hello'); } getSubject() { return Promise.resolve(this.subject.name); } getPunctuation() { return Promise.resolve('!'); } }
  • 72.
    Component export class SimpleComponentimplements OnInit { subject: string; greeting: string; punctuation: string; constructor(private service: GreetingService) { } ngOnInit() { this.service.getGreeting() .then(res => this.greeting = res); this.service.getSubject() .then(res => this.subject = res); this.service.getPunctuation() .then(res => this.punctuation = res); } }
  • 73.
    Component export class SimpleComponentimplements OnInit { subject: string; greeting: string; punctuation: string; constructor(private service: GreetingService) { } ngOnInit() { this.service.getGreeting() .then(res => this.greeting = res); this.service.getSubject() .then(res => this.subject = res); this.service.getPunctuation() .then(res => this.punctuation = res); } }
  • 74.
    Setup beforeEach(async(() => { fixture= TestBed.configureTestingModule({ declarations: [SimpleComponent ], providers: [ GreetingService ] }) .createComponent(SimpleComponent) .then(() => { component = fixture.componentInstance; element = fixture.debugElement; greetingService = de.injector.get(GreetingService); return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); }));
  • 75.
    Setup beforeEach(async(() => { fixture= TestBed.configureTestingModule({ declarations: [SimpleComponent ], providers: [ GreetingService ] }) .createComponent(SimpleComponent) .then(() => { component = fixture.componentInstance; element = fixture.debugElement; greetingService = de.injector.get(GreetingService); return fixture.whenStable().then(() => { fixture.detectChanges(); }); }); }));
  • 76.
    Spy with fixture.whenStable it('gets`greeting` after promise (async)', async(() => { spyOn(greetingService, 'getGreeting') .and.returnValue(Promise.resolve('Greetings')); expect(component.greeting).toBe('Greetings'); }); }));
  • 77.
    Spy with fixture.whenStable it('gets`greeting` after promise (async)', async(() => { spyOn(greetingService, 'getGreeting') .and.returnValue(Promise.resolve('Greetings')); expect(component.greeting).toBe('Greetings'); }); }));
  • 78.
    Spy with fixture.whenStable it('gets`greeting` after promise (async)', async(() => { spyOn(greetingService, 'getGreeting') .and.returnValue(Promise.resolve('Greetings')); expect(component.greeting).toBe('Greetings'); }); }));
  • 79.
    Spy with fixture.whenStable it('gets`greeting` after promise (async)', async(() => { spyOn(greetingService, 'getGreeting') .and.returnValue(Promise.resolve('Greetings')); expect(component.greeting).toBe('Greetings'); }); }));
  • 80.
  • 81.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 82.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 83.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 84.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 85.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 86.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 87.
    Spy with tick it('gets`subject` after promise (fakeAsync)', fakeAsync(() => { spyOn(greetingService, 'getSubject') .and.returnValue(Promise.resolve('universe')); fixture.detectChanges(); tick(); fixture.detectChanges(); expect(component.subject).toBe('universe'); }));
  • 88.
  • 89.
    http://digitaldrummerj.me Component with Inputsand Outputs The goal is to ensure that binding works as expected Test input: simply update value and ensure it renders Test output: trigger triggerEventHandler and subscribe to the EventEmitter
  • 90.
    @Component({ selector: 'app-input-output', template: ` <h1>Hello{{subject}}!</h1> <button (click)="depart()">We Out</button> ` }) export class InputOutputComponent { @Input('subject') subject: string; @Output('leave') leave: EventEmitter<string> = new EventEmitter(); depart() { this.leave.emit(`Later ${this.subject}!`); } } Component
  • 91.
    @Component({ selector: 'app-input-output', template: ` <h1>Hello{{subject}}!</h1> <button (click)="depart()">We Out</button> ` }) export class InputOutputComponent { @Input('subject') subject: string; @Output('leave') leave: EventEmitter<string> = new EventEmitter(); depart() { this.leave.emit(`Later ${this.subject}!`); } } Component
  • 92.
    beforeEach(() => { fixture= TestBed.configureTestingModule({ declarations: [ InputOutputComponent ] }) .createComponent(InputOutputComponent); component = fixture.componentInstance; de = fixture.debugElement; button = de.query(By.css('button')); component.subject = 'galaxy'; fixture.detectChanges(); }); Setup
  • 93.
    beforeEach(() => { fixture= TestBed.configureTestingModule({ declarations: [ InputOutputComponent ] }) .createComponent(InputOutputComponent); component = fixture.componentInstance; de = fixture.debugElement; button = de.query(By.css('button')); component.subject = 'galaxy'; fixture.detectChanges(); }); Setup
  • 94.
    beforeEach(() => { fixture= TestBed.configureTestingModule({ declarations: [ InputOutputComponent ] }) .createComponent(InputOutputComponent); component = fixture.componentInstance; de = fixture.debugElement; button = de.query(By.css('button')); component.subject = 'galaxy'; fixture.detectChanges(); }); Setup
  • 95.
    it('has `subject` asan @Input', () => { expect(component.subject).toBe('galaxy'); }); Input
  • 96.
    it('says goodbye tothe `subject`', () => { let farewell; component.leave.subscribe(event => farewell = event); button.triggerEventHandler('click', { button: 0 }); expect(farewell).toBe('Later galaxy!'); }); Output
  • 97.
    it('says goodbye tothe `subject`', () => { let farewell; component.leave.subscribe(event => farewell = event); button.triggerEventHandler('click', { button: 0 }); expect(farewell).toBe('Later galaxy!'); }); Output
  • 98.
    it('says goodbye tothe `subject`', () => { let farewell; component.leave.subscribe(event => farewell = event); button.triggerEventHandler('click', { button: 0 }); expect(farewell).toBe('Later galaxy!'); }); Output
  • 99.
  • 100.
    http://digitaldrummerj.me Routed Component Avoid routercomplexity. Use RouterTestingModule Test that component navigates to proper route
  • 101.
    beforeEach(() => { fixture= TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ {path: '', redirectTo: '/items', pathMatch: 'full' }, {path: 'items', component: ItemsComponent}, {path: 'routed/:subject', component: RoutedComponent}, {path: 'widgets', component: WidgetsComponent}, {path: '**', redirectTo: '/items', pathMatch: 'full'} ]); ], declarations: [ RoutedComponent ] }) .createComponent(RoutedComponent); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); }); Routes
  • 102.
    import { RouterTestingModule} from '@angular/router/testing'; beforeEach(() => { fixture = TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ {path: '', redirectTo: '/items', pathMatch: 'full' }, {path: 'items', component: ItemsComponent}, {path: 'routed/:subject', component: RoutedComponent}, {path: 'widgets', component: WidgetsComponent}, {path: '**', redirectTo: '/items', pathMatch: 'full'} ]); ], declarations: [ RoutedComponent ] }) .createComponent(RoutedComponent); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); }); Routes
  • 103.
    import { RouterTestingModule} from '@angular/router/testing'; beforeEach(() => { fixture = TestBed.configureTestingModule({ imports: [ RouterTestingModule.withRoutes([ {path: '', redirectTo: '/items', pathMatch: 'full' }, {path: 'items', component: ItemsComponent}, {path: 'routed/:subject', component: RoutedComponent}, {path: 'widgets', component: WidgetsComponent}, {path: '**', redirectTo: '/items', pathMatch: 'full'} ]); ], declarations: [ RoutedComponent ] }) .createComponent(RoutedComponent); component = fixture.componentInstance; de = fixture.debugElement; fixture.detectChanges(); }); Routes
  • 104.
    RouterLink import { RouterLinkWithHref} from '@angular/router'; import { fakeAsync } from '@angular/core/testing'; it('should have 1 routerLink in template', fakeAsync(() => { allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); expect(allLinks.length).toBe(1, 'should have 1 link'); }));
  • 105.
    RouterLink import { RouterLinkWithHref} from '@angular/router'; import { fakeAsync } from '@angular/core/testing'; it('should have 1 routerLink in template', fakeAsync(() => { allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); expect(allLinks.length).toBe(1, 'should have 1 link'); }));
  • 106.
    RouterLink import { RouterLinkWithHref} from '@angular/router'; import { fakeAsync } from '@angular/core/testing'; it('should have 1 routerLink in template', fakeAsync(() => { allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); expect(allLinks.length).toBe(1, 'should have 1 link'); }));
  • 107.
    RouterLink import { RouterLinkWithHref} from '@angular/router'; import { fakeAsync } from '@angular/core/testing'; it('should have 1 routerLink in template', fakeAsync(() => { allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); expect(allLinks.length).toBe(1, 'should have 1 link'); }));
  • 108.
    RouterLink import { RouterLinkWithHref} from '@angular/router'; import { fakeAsync } from '@angular/core/testing'; it('should have 1 routerLink in template', fakeAsync(() => { allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); expect(allLinks.length).toBe(1, 'should have 1 link'); }));
  • 109.
    Location Check import {fakeAsync } from '@angular/core/testing'; it('all items takes me home', fakeAsync(() => { const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const linkDes = allLinkDes[0]; expect(location.path()).toBe('', 'link should not have navigated yet'); linkDes.triggerEventHandler('click', { button: 0 }); tick(); expect(location.path()).toBe('/items'); }));
  • 110.
    Location Check import {fakeAsync } from '@angular/core/testing'; it('all items takes me home', fakeAsync(() => { const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const linkDes = allLinkDes[0]; expect(location.path()).toBe('', 'link should not have navigated yet'); linkDes.triggerEventHandler('click', { button: 0 }); tick(); expect(location.path()).toBe('/items'); }));
  • 111.
    Location Check import {fakeAsync } from '@angular/core/testing'; it('all items takes me home', fakeAsync(() => { const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const linkDes = allLinkDes[0]; expect(location.path()).toBe('', 'link should not have navigated yet'); linkDes.triggerEventHandler('click', { button: 0 }); tick(); expect(location.path()).toBe('/items'); }));
  • 112.
    Location Check import {fakeAsync } from '@angular/core/testing'; it('all items takes me home', fakeAsync(() => { const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const linkDes = allLinkDes[0]; expect(location.path()).toBe('', 'link should not have navigated yet'); linkDes.triggerEventHandler('click', { button: 0 }); tick(); expect(location.path()).toBe('/items'); }));
  • 113.
    Location Check import {fakeAsync } from '@angular/core/testing'; it('all items takes me home', fakeAsync(() => { const allLinks = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref)); const linkDes = allLinkDes[0]; expect(location.path()).toBe('', 'link should not have navigated yet'); linkDes.triggerEventHandler('click', { button: 0 }); tick(); expect(location.path()).toBe('/items'); }));
  • 114.
    http://digitaldrummerj.me Document FunctionalityPinpoint Bugs CheckRegressionConfirm Functionality Unit Test Will Make You Faster
  • 115.
  • 116.
  • 117.
    http://digitaldrummerj.me Resources Angular Unit TestingGuide: angular.io/guide/testing Code Repository: github.com/digitaldrummerj/angular-tutorial-code/tree/chapter- unit-test Slides: slideshare.net/digitaldrummerj/angular-unit-testing-from-the- trenches
  • 118.
  • 119.

Editor's Notes

  • #14 Modules – building blocks that contain routes, components, services, and more. Components – contains a template, data and logic forming part of a DOM tree. Routing – Renders a component based on the URL state, drives navigation. Services – Data layer, logic that is not component related, such as API requests.
  • #15 Done http://sailsjs.com/get-started
  • #17 Test runner that is used to execute Angular unit tests Installed and configured by default with Angular CLI project Configured via the karma.conf.js file Tests (specs) are identified with a .spec.ts naming convention