When setting up dependency injection for my demo project I follwed this excellent blog post.
This post demonstrates how to unify boilerplate necessary for dependency injection away from your test specs into a single file.
What boilerplate?
Below is a very simple test spec with dependency injection component we’re testing. As you can see, it’s 99% boilerplate, lots of imports along with the beforeEach
and beforeEachProvider
functions.
import {
beforeEach,
beforeEachProviders,
describe,
expect,
injectAsync,
it,
} from '@angular/core/testing';
import {
ComponentFixture,
TestComponentBuilder,
} from '@angular/compiler/testing';
import { provide } from '@angular/core';
import { Page2 } from './page2';
import { Utils } from '../../services/utils';
import { ConfigMock } from './mocks';
import {
Config,
Form,
App,
NavController,
NavParams,
Platform,
} from 'ionic-angular';
let page2: Page2 = null;
let page2Fixture: ComponentFixture = null;
describe('Page2', () => {
beforeEachProviders(() => [
Form,
provide(NavController, {useClass: MockClass}),
provide(NavParams, {useClass: MockClass}),
provide(Config, {useClass: MockClass}),
provide(App, {useClass: MockClass}),
provide(Platform, {useClass: MockClass}),
]);
beforeEach(injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb
.createAsync(Page2)
.then((componentFixture: ComponentFixture) => {
page2Fixture = componentFixture;
page2 = componentFixture.componentInstance;
page2Fixture.detectChanges();
})
.catch(Utils.promiseCatchHandler);
}));
it('initialises', () => {
expect(page2).not.toBeNull();
expect(page2Fixture).not.toBeNull();
});
});
For a few files this is fine. In a larger project with tens of spec files, maintaining this boilerplate became a headache for us; Angular 2 and Ionic 2 were evolving and the imports and providers kept changing.
It’s also not great having to read ~50 lines of boilerplate at the top of each file before you can see what’s being tested.
Unifying the boilerplate
We set out to remove as many of the imports as we could, as well as outsourcing the beforeEach
functions into a single file.
Here’s the what we ended up with:
import { provide, Type } from '@angular/core';
import { ComponentFixture, TestComponentBuilder } from '@angular/compiler/testing';
import { injectAsync } from '@angular/core/testing';
import { Control } from '@angular/common';
import { App, Config, Form, NavController, Platform } from 'ionic-angular';
import { ClickersMock, ConfigMock, NavMock } from './mocks';
import { Utils } from '../app/services/utils';
import { Clickers } from '../app/services/clickers';
export { TestUtils } from './testUtils';
export let providers: Array<any> = [
Form,
provide(Config, {useClass: ConfigMock}),
provide(Clickers, {useClass: ClickersMock}), // required by ClickerButton
provide(App, {useClass: ConfigMock}), // required by ClickerList
provide(NavController, {useClass: NavMock}), // required by ClickerList
provide(Platform, {useClass: ConfigMock}), // -> IonicApp
];
export let injectAsyncWrapper: Function = ((callback) => injectAsync([TestComponentBuilder], callback));
export let asyncCallbackFactory: Function = ((component, testSpec, detectChanges, beforeEachFn) => {
return ((tcb: TestComponentBuilder) => {
return tcb.createAsync(component)
.then((fixture: ComponentFixture<Type>) => {
testSpec.fixture = fixture;
testSpec.instance = fixture.componentInstance;
testSpec.instance.control = new Control('');
if (detectChanges) fixture.detectChanges();
if (beforeEachFn) beforeEachFn(testSpec);
})
.catch(Utils.promiseCatchHandler);
});
});
injectAsyncWrapper
wraps injectAsync
, executing the callback passed in when TestComponentBuilder has completed. The main win from this is that you don’t need to import TestComponentBuilder
in each of your specs.
The heavy lifting is done in asyncCallbackFactory
, which takes the following arguments:
- component: The coponent class we’re currently testing
- testSpec: A reference to the test spec. This is used so we can set the
component
(instance) andfixture
variables in the test spet without needing further boilerplate. - detectChanges: Should we invoke
detectChanges()
against the component fixture inbeforeEach
? This is useful in cases when you need to do initial / bespoke setup on the component instance before detecting changes. Usually this would betrue
. - beforeEachFn: Sometimes you need to run an additional function before each test, nothing to do with DI, your standard beforeEach. Before this change you’d have this code in-lined inside createAsync callback.
This diff is a good example of the above arguments in use after the boilerplate has been removed.
Resultant Spec
As we import from our unified diExports
file, most of the boilerplate has been removed:
import { beforeEach, beforeEachProviders, describe, expect, it } from '@angular/core/testing';
import { asyncCallbackFactory, injectAsyncWrapper, providers, TestUtils } from '../../../test/diExports';
import { Page2 } from './page2';
this.fixture = null;
this.instance = null;
describe('Page2', () => {
beforeEachProviders(() => providers);
beforeEach(injectAsyncWrapper(asyncCallbackFactory(Page2, this, true)));
it('initialises', () => {
expect(this.instance).not.toBeNull();
expect(this.fixture).not.toBeNull();
});
});
You can see the full commit of the above change here.
Contribute
Clickers is a work in progress. If you’d like to help out or have any suggestions, check the [roadmap sticky][clicker-issue-38].
This blog is on github, if you can improve it, have any suggestions or I’ve failed to keep it up to date, raise an issue or a PR.
Help!
If you can’t get any of this working in your own project, [raise an issue][clicker-issue-new] and I’ll do my best to help out.