WE ARE THE JSLEAGUE
22 feb 2020
Wild Code School
Whoami
Frontend Developer, ThisDot Labs
organizer for ngBucharest
@ngBucharest
groups/angularjs.bucharest
Test frameworks - where you write your test
Jasmine, Mocha, Tape
Test environment - where your tests are executed
browsers - Chrome, Firefox, etc
headless browsers - JSDom, PhantomJS, Puppeteer
Test runners - where you run your tests
Karma, Jest
npm install --save-dev jest
Behaviour Driven Development (BDD) Testing Framework
Create hierarchical suites of test - describe('', function)
The tests are written as specifications - it('', function)
Expectations and Matchers (built-in and custom) - expect(x).toBe(expected)
Spies - a test double pattern
Asynchronous operations support
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);
});
});
Runs before each test (it clause) - good place to initialize test data
Runs after each test - good place to free any used resources
Expectation
Matcher
const mockObj = {
mockFn: jest.fn()
};
mockObj.mockFn('test');
expect(mockObj.mockFn).toHaveBeenCalled();
expect(mockObj.mockFn).toHaveBeenCalledTimes(1);
expect(mockObj.mockFn).toHaveBeenCalledWith('test');
const mockObj = {
mockFn: (value) => { this.prop = value; },
prop: false
};
mockObj.mockFn(true);
const spy = jest.spyOn(mockObj, 'mockFn');
expect(spy).toHaveBeenCalled();
expect(mockObj.prop).toBe(true);
Mocking implementatons
Spying on methods
describe('Asynchronous specs', () => {
let value;
beforeEach(() => {
value = 0;
});
it('should support async execution', done => {
setTimeout(() => {
value++;
expect(value).toBeGreaterThan(0);
done();
}, 1000);
});
});
Call the done method when the async test finished
jasmine.createSpyObj('name', ['key']) --> jest.fn({key: jest.fn()})
jasmine.createSpy('name') --> jest.fn()
and.returnValue() --> mockReturnValue()
spyOn(...).and.callFake(() => {}) --> jest.spyOn(...).mockImplementation(() => {})
jasmine.any, jasmine.objectContaining, etc. --> expect.any, expect.objectContaining
Arrange all necessary preconditions and inputs.
Act on the object or method under test.
Assert that the expected results have occurred.
DRY vs DAMP
TECHNIQUES
RULES
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
heroDetCmp.ngOnInit();
});
describe('ngOninit' function() {
it("should set the hero", function() {
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
expect(heroDetCmp.heroId).toBe(3));
});
});
});
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
});
describe('ngOninit' function() {
it("should set the hero", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.heroId).toBe(3));
});
});
});
Isolated tests: only the class, mocking everything
Integration tests: compiling components and using the injector
Shallow: mock out related components
Deep: include all components
@Component({
template: `
<h1>{{message}}</h1>
<button (click)="clearMessage">Clear</button>
`
})
export class GreetComponent {
public message = '';
constructor() {}
setMessage(newMessage: string) {
this.message = newMessage;
}
clearMessage() {
this.message = '';
}
}
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('');
});
});
TestBed - a harness for compiling components
inject() - provides access to injectables
async() & fakeAsync() - async Zone control
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HeroComponent ],
imports: [ ... ],
providers: [ ... ],
Schemas: [ ... ]
});
});
});
TestBed configures a temporary NgModule for testing
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({...});
TestBed.overrideComponent(GreetComponent, {
set: {
template: '<div>Overridden template here</div>'
// ...
}
});
});
});
TestBed configurations can be overriden
describe('Testing GreetComponent', () => {
let component: GreetComponent;
let fixture: ComponentFixture<GreetComponent>;
beforeEach(() => {
TestBed.configureTestingModule({...});
fixture = TestBed.createComponent(GreetComponent);
});
});
Access to the component, its DOM and change detection
componentInstance - the instance of the component created by TestBed
debugElement - provides insight into the component and its DOM element
nativeElement - the native DOM element at the root of the component
detectChanges() - trigger a change detection cycle for the component
whenStable() - returns a promise that resolves when the fixture is stable
describe('Testing message state in greet.component', () => {
beforeEach(...)
it('should display original greet', () => {
fixture.detectChanges();
expect(element.textContent).toContain(component.message);
});
})
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Configure automatic change detection
describe('Testing message state in greet.component', () => {
beforeEach(...)
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('');
});
})
Insights into the component's DOM representation
parent / children - the immediate parent or children of this DebugElement
query(predicate) - search for one descendant that matches
queryAll(predicate) - search for many descendants that match
injector - this component's injector
listeners - this callback handlers for this component's events and @Outputs
triggerEventHandler(listener) - trigger an event or @Output
nativeElement provides:
querySelector(cssSelector)
debugElement provides:
query(predicate)
queryAll(predicate)
predicates can be created by helpers:
By.css(selector)
By.directive(DirectiveType)
let heroService;
beforeEach(() => {
heroService = TestBed.inject(HeroService);
}));
TestBed.inject(Type)
Search for instances of the child component:
movieElements =
fixture.debugElement.queryAll(By.directive(MovieItemComponent));
Check the value of @Input properties on the child component :
expect(movieElements[0].componentInstance.movie).toBe(MOVIES[0]);
Trigger @Output bindings:
movieElements[0].triggerEventHandler('delete', null);
Goal - Test if inputs and outputs work correctly
Approaches
test as a standalone component
test inside a container component
@Component({
selector: 'greet-message',
template: `<div class="greet">
{{message}}
<button (click)="handleClick()">LIKE</button>
</div>`
})
export class GreetComponent {
@Input() message: string;
@Output() onLiked = new EventEmitter<string>();
handleClick() {
this.onLiked.emit(this.message);
}
}
• Inputs - set a value to the input property on the component object
it('should display greeting', () => {
expect(greetElementText.nativeElement.textContent).toBe(expectedMessage);
});
• Outputs - subscribe to EventEmitter, trigger click event
it('should raise selected event when clicked', () => {
let likedMessage: string;
component.onLiked.subscribe((message: string) => {
likedMessage = message;
});
greetElementButton.triggerEventHandler('click', null);
expect(likedMessage).toBe(expectedMessage);
});
Test in host component - create an on the fly component to test the target component
@Component({
template: `
<greet-message [message]="greet" (onLiked)="handleLike($event)">
</greet-message>
`,
})
class TestHostComponent {
greet = 'Wassuuuup?!?';
handleLike(message: string) {
this.greet = 'New greet';
}
}
describe('Test input/output for components', () => {
let fixture: ComponentFixture<TestHostComponent>;
let testHost: TestHostComponent;
let greetElementText: DebugElement;
let greetElementButton: DebugElement;
beforeEach(
async(() => {
TestBed.configureTestingModule({
declarations: [GreetComponent, TestHostComponent],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
greetElementText = fixture.debugElement.query(By.css('.greet span'));
greetElementButton = fixture.debugElement.query(By.css('.greet button'));
fixture.detectChanges();
});
...
});
Declare both components in the TestBed
Only create the test host component
• Inputs - set a value to the input property on the component object
it('should display greeting', () => {
const expectedGreet = testHost.greet;
expect(greetElementText.nativeElement.textContent).toBe(expectedGreet);
});
• Outputs - subscribe to EventEmitter, trigger click event
it('should raise selected event when clicked', () => {
let likedMessage: string;
component.onLiked.subscribe((message: string) => {
likedMessage = message;
});
greetElementButton.triggerEventHandler('click', null);
expect(likedMessage).toBe(expectedMessage);
});
import {Component} from '@angular/core';
import {UserService} from './user.service';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.message = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
The component injects a service
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ]
providers: [{
provide: UserService,
useValue: userServiceStub
}]
});
userService = TestBed.inject(UserService);
A stub is just a replacement for the actual service. We can only declare the properties/methods that will be in use for the current test suite
Always access service from injector, never directly (userService Stub) - the injected service is a clone
describe('HttpClient testing', () => {
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
httpTestingController = TestBed.inject(HttpTestingController);
});
});
Mock the Http service
HttpClientTestingModule - don't use the regular HttpClientModule
HttpTestingController - used to control the HTTP calls
it('should get the proper todo\'s', () => {
const testData: Data = [{todo: 'Test Data'}];
// when call is made, observable emits
component.todos$.subscribe(data =>
expect(data).toEqual(testData)
);
// Match request URL's
const req = httpTestingController.expectOne('/data');
// Assert request method
expect(req.request.method).toEqual('GET');
// respond with mock data
req.flush(testData);
// make sure no outstanding calls
httpTestingController.verify();
});
Intercepts and tracks asynchronous callbacks
Configured by rules (or specs)
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private greetingsService: GreetingsService) { }
ngOnInit() {
this.greetingsService.getGreets()
.then(greets => this.message = greets[0]);
}
}
it('should show message (async)',
async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
});
}),
);
it('should show message (fakeAsync)',
fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
}),
);
running the test in a special async test zone
allows for a more linear coding style, no need for then()-s
can only be called inside a fakeAsync, “waits” for all async operations to finish