Standalone component testing is easy, but you will rarely need to write meaningful tests for a component that exists in isolation. More often than not, the component will have one or many dependencies, and writing good unit tests is the difference between delight and despair.
The code, links, and a live example related to this recipe are available at .
Suppose you already have the service from the Unit testing a synchronous service recipe. In addition, you have a component, which makes use of this service:
[src/app/magic-eight-ball/magic-eight-ball.component.ts] import {Component} from '@angular/core'; import {MagicEightBallService} from '../magic-eight-ball.service'; @Component({ selector: 'app-magic-eight-ball', template: ` <button (click)="update()">Click me!</button> <h1>{{ result }}</h1> ` }) export class MagicEightBallComponent { result: string = ''; constructor(private magicEightBallService_: MagicEightBallService) {} update() { this.result = this.magicEightBallService_.reveal(); } }
Your objective is to write a suite of unit tests for this component without setting an explicit dependency on the service.
Begin with a skeleton of your test file:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; describe('Component: MagicEightBall', () => { beforeEach(async(() => { })); afterEach(() => { }); it('should begin with no text', async(() => { })); it('should show text after click', async(() => { })); });
You'll first want to configure the test module so that it properly provides these imported targets in the test:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; describe('Component: MagicEightBall', () => { let fixture; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ MagicEightBallService ] }); fixture = TestBed.createComponent(MagicEightBallComponent); })); afterEach(() => { fixture = undefined; }); it('should begin with no text', async(() => { })); it('should show text after click', async(() => { })); });
Injecting the actual service works just fine, but this isn't what you want to do. You don't want to actually inject an instance of MagicEightBallService
into the component, as that would set a dependency on the service and make the unit test more complicated than it needs to be. However, MagicEightBallComponent
needs to import something that resembles a MagicEightBallService
. An excellent solution here is to create a service stub and inject it in its place:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; describe('Component: MagicEightBall', () => { let fixture; let magicEightBallResponse = 'Answer unclear'; let magicEightBallServiceStub = { reveal: () => magicEightBallResponse }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ { provide: MagicEightBallService, useValue: magicEightBallServiceStub } ] }); fixture = TestBed.createComponent(MagicEightBallComponent); })); afterEach(() => { fixture = undefined; }); it('should begin with no text', async(() => { })); it('should show text after click', async(() => { })); });
A component can't tell the difference between the actual service and its mock, so it will behave normally in the test conditions you've set up.
Next, you should write the preclick test by checking that the fixture's nativeElement
contains no text:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallServiceStub = { reveal: () => magicEightBallResponse }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ MagicEightBallComponent ], providers: [ { provide: MagicEightBallService, useValue: magicEightBallServiceStub } ] }); fixture = TestBed.createComponent(MagicEightBallComponent); })); afterEach(() => { fixture = undefined; }); it('should begin with no text', async(() => { fixture.detectChanges(); expect(getHeaderEl().textContent).toEqual(''); })); it('should show text after click', async(() => { })); });
For the second test, you should trigger a click on the button, instruct the fixture to perform change detection, and then inspect the DOM to see that the text was properly inserted. Since you have defined the text that the stub will return, you can just compare it directly with that:
[src/app/magic-eight-ball/magic-eight-ball.component.spec.ts] import {TestBed, async} from '@angular/core/testing'; import {MagicEightBallComponent} from './magic-eight-ball.component'; import {MagicEightBallService} from '../magic-eight-ball.service'; import {By} from '@angular/platform-browser'; describe('Component: MagicEightBall', () => { let fixture; let getHeaderEl = () => fixture.nativeElement.querySelector('h1'); let magicEightBallResponse = 'Answer unclear'; let magicEightBallServiceStub = { reveal: () => magicEightBallResponse }; ... it('should begin with no text', async(() => { expect(getHeaderEl().textContent).toEqual(''); })); it('should show text after click', async(() => { fixture.debugElement.query(By.css('button')) .triggerEventHandler('click', null); fixture.detectChanges(); expect(getHeaderEl().textContent) .toEqual(magicEightBallResponse); })); });
You'll note that this needs to import and use the By.css
predicate, which is required to perform DebugElement
inspections.
As demonstrated in the dependency injection chapter, providing a stub to the component is no different than providing a regular value to the core application.
The stub here is a single function that returns a static value. There is no concept of randomly selecting from the service's array of strings, and there doesn't need to be. The unit tests for the service itself ensure that it is behaving properly. Instead, the only value provided by the service here is the information it passes back to the component for interpolation back into the template.