How it works: Angular Tests

Angular tests are always in the same place as the code. When you create a component, service, pipe, directive with ng generate it will always also generate a .spec.ts file.

Every file with the extension .spec.ts is a test file that is caught by the testing tool. Each thing is tested in a different way. For a really long documentation on tests check the official documentation here.

Before anything, note that "zone.js": "^0.8.13" has a bug when testing with event binding and should be avoided, use 0.8.14+ or downgrade to 0.8.12.

Note that on services tests I provide some of async workarounds. They can be avoided by using jasmine-promises non official package, since the official team for some reason does not want to implement async/await keywords properly.

Component

Given a component with a name field and a submit button

Template

<form (ngSubmit)="submit($event)" [formGroup]="form" novalidate>
<input type="text" formControlName="name" />
<button type="submit">Show hero name</button>
</form>

Component

import { FormControl, FormGroup, Validators } from '@angular/forms';

import { Component } from '@angular/core';

@Component({
selector: 'app-hero',
templateUrl: 'hero.component.html',
})
export class HeroComponent {
public form = new FormGroup({
name: new FormControl('', Validators.required),
});

constructor() { }

submit(event) {
console.log(event);
console.log(this.form.controls.name.value);
}
}

We can test it like this:

import { ComponentFixture, TestBed, async } from '@angular/core/testing';

import { HeroComponent } from './hero.component';
import { ReactiveFormsModule } from '@angular/forms';

describe('HeroComponent', () => {
let component: HeroComponent;
let fixture: ComponentFixture<HeroComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [HeroComponent],
imports: [ReactiveFormsModule],
}).compileComponents();

fixture = TestBed.createComponent(HeroComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));

it('should be created', () => {
expect(component).toBeTruthy();
});

it('should log hero name in the console when user submit form', async(() => {
const heroName = 'Saitama';
const element = <HTMLFormElement>fixture.debugElement.nativeElement.querySelector('form');

spyOn(console, 'log').and.callThrough();

component.form.controls['name'].setValue(heroName);

element.querySelector('button').click();

fixture.whenStable().then(() => {
fixture.detectChanges();
expect(console.log).toHaveBeenCalledWith(heroName);
});
}));


it('should validate name field as required', () => {
component.form.controls['name'].setValue('');
expect(component.form.invalid).toBeTruthy();
});
});

Service

Given a service that can login/logout/create account like this

import 'rxjs/add/operator/toPromise';

import { Http } from '@angular/http';
import { Injectable } from '@angular/core';

interface LoginCredentials {
password: string;
user: string;
}

interface UserAccount extends LoginCredentials {
name: string;
email: string;
}

@Injectable()
export class AuthService {
constructor(private http: Http) { }

async signIn({ user, password }: LoginCredentials) {
const response = await this.http.post('/login', {
password,
user,
}).toPromise();

return response.json();
}

async signOut() {
const response = await this.http.delete('/logout').toPromise();
return response.json();
}

async createAccount(account: UserAccount) {
const response = await this.http.post('/account', account)
.toPromise();
return response.json();
}
}

We can test it like this (there are no tests for signOut and createAccount)

import { ConnectionBackend, Http, HttpModule, Response, ResponseOptions } from '@angular/http';
import { TestBed, async, inject } from '@angular/core/testing';

import { AuthService } from './auth.service';
import { MockBackend } from '@angular/http/testing';
import { MockConnection } from '@angular/http/testing';

describe('AuthService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpModule],
providers: [
AuthService,
Http,
{ provide: ConnectionBackend, useClass: MockBackend },
]
});
});

it('should be created', inject([AuthService], (service: AuthService) => {
expect(service).toBeTruthy();
}));

it('should login user if right credentials are passed', async(
inject([AuthService], async (authService) => {
const backend: MockBackend = TestBed.get(ConnectionBackend);
const http: Http = TestBed.get(Http);

backend.connections.subscribe((c: MockConnection) => {
c.mockRespond(
new Response(
new ResponseOptions({
body: {
accessToken: 'abcdef',
},
}),
),
);
});

const result = await authService.signIn({ password: 'ok', user: 'bruno' });

expect(result).toEqual({
accessToken: 'abcdef',
});
}))
);
});

The same test case can be written in two other ways

it('should login user if right credentials are passed', async(async () => {
const backend: MockBackend = TestBed.get(ConnectionBackend);
const http: Http = TestBed.get(Http);

backend.connections.subscribe((c: MockConnection) => {
c.mockRespond(
new Response(
new ResponseOptions({
body: {
accessToken: 'abcdef',
},
}),
),
);
});

const authService: AuthService = TestBed.get(AuthService);

const result = await authService.signIn({ password: 'ok', user: 'bruno' });

expect(result).toEqual({
accessToken: 'abcdef',
});
}));

it('should login user if right credentials are passed', async (done) => {
const authService: AuthService = TestBed.get(AuthService);

const backend: MockBackend = TestBed.get(ConnectionBackend);
const http: Http = TestBed.get(Http);

backend.connections.subscribe((c: MockConnection) => {
c.mockRespond(
new Response(
new ResponseOptions({
body: {
accessToken: 'abcdef',
},
}),
),
);
});

try {
const result = await authService.signIn({ password: 'ok', user: 'bruno' });

expect(result).toEqual({
accessToken: 'abcdef',
});

done();
} catch (err) {
fail(err);
done();
}
});

Pipe

Given a pipe that reverse a string

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'reverse' })
export class ReversePipe implements PipeTransform {
transform(value: string): string {
return value.split('').reverse().join('');
}
}

We can test it like this

import { TestBed, inject } from '@angular/core/testing';

import { ReversePipe } from './reverse.pipe';

describe('ReversePipe', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ReversePipe],
});
});

it('should be created', inject([ReversePipe], (reversePipe: ReversePipe) => {
expect(reversePipe).toBeTruthy();
}));

it('should reverse a string', inject([ReversePipe], (reversePipe: ReversePipe) => {
expect(reversePipe.transform('abc')).toEqual('cba');
}));
});

Directive

Given a directive that will change the background color of the element

import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
@Input('appHighlight') // tslint:disable-line no-input-rename
highlightColor: string;

constructor(private el: ElementRef) { }

@HostListener('mouseenter')
onMouseEnter() {
this.highlight(this.highlightColor || 'red');
}

@HostListener('mouseleave')
onMouseLeave() {
this.highlight(null);
}

private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}

We can test it like this

import { ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing';

import { Component } from '@angular/core';
import { HighlightDirective } from './highlight.directive';

@Component({
selector: 'app-test-container',
template: `
<div>
<span id="red">red text</span>
<span id="green">green text</span>
<span id="no">no color</span>
</div>
`
})
class ContainerComponent { }

const mouseEvents = {
get enter() {
const mouseenter = document.createEvent('MouseEvent');
mouseenter.initEvent('mouseenter', true, true);
return mouseenter;
},
get leave() {
const mouseleave = document.createEvent('MouseEvent');
mouseleave.initEvent('mouseleave', true, true);
return mouseleave;
},
};

describe('HighlightDirective', () =&gt; {
let fixture: ComponentFixture;
let container: ContainerComponent;
let element: HTMLElement;

beforeEach(() =&gt; {
TestBed.configureTestingModule({
declarations: [ContainerComponent, HighlightDirective],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true },
],
});

fixture = TestBed.createComponent(ContainerComponent);
// fixture.detectChanges(); // without the provider
container = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should set background-color to empty when mouse leaves with directive without arguments', () =&gt; {
const targetElement = element.querySelector('#red');

targetElement.dispatchEvent(mouseEvents.leave);
expect(targetElement.style.backgroundColor).toEqual('');
});

it('should set background-color to empty when mouse leaves with directive with arguments', () =&gt; {
const targetElement = element.querySelector('#green');

targetElement.dispatchEvent(mouseEvents.leave);
expect(targetElement.style.backgroundColor).toEqual('');
});

it('should set background-color red with no args passed', () =&gt; {
const targetElement = element.querySelector('#red');

targetElement.dispatchEvent(mouseEvents.enter);
expect(targetElement.style.backgroundColor).toEqual('red');
});

it('should set background-color green when passing green parameter', () =&gt; {
const targetElement = element.querySelector('#green');

targetElement.dispatchEvent(mouseEvents.enter);
expect(targetElement.style.backgroundColor).toEqual('green');
});
});

Angular How it works series

Check out other posts of this series, Angular How it works.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade