How to mock dependencies of initialization logic
This article describes how to mock dependencies of initialization logic.
Basically, how to mock a Service and/or how to mock an InjectionToken
which are injected in a constructor as dependencies.
Also, this article covers how to change mocks to provide different values for the initialization logic.
Let's imagine we have a declaration with dependencies. Usually, the declaration is a component, directive, pipe, service or even a module, and its dependencies are services, tokens, or, even more advanced logic, components and directives on the same host element.
class TargetComponent {
// A property which will be used somewhere else: in a template or wherever.
public name: string;
// Required dependencies.
constructor(
@Inject(CONFIG) config: ConfigInterface,
user: CurrentUserService,
) {
// Business logic in the constructor to calculate the name.
if (config.displayName === 'first') {
this.name = user.firstName;
} else {
this.name = user.lastName;
}
}
}
I guess, you have spotted an issue here. Right, it can require much boilerplate to mock and customize the dependencies, because they are used in the constructor.
The main disadvantages and pain of testing initialization logic in Angular declarations:
- many additional mostly copy-pasted
TestBed.configureTestingModulewith slight differences for each use case - additional
beforeEachblock withTestBed.injectto set values TestBed.injectdoesn't work with host dependenciesTestBed.injectdoesn't allow to change primitive values
To make it a joy, ng-mocks provides MockInstance
which can be placed in each it before MockRender or TestBed.createComponent
to set values,
and it does support customization of all mock dependencies: InjectionToken, Service
or host dependencies such as Component, Directive or host providers.
// It is required if you cannot use default customizations.
// https://ng-mocks.sudo.eu/extra/install#default-customizations
// After each test it removes customizations which are done by MockInstance.
MockInstance.scope();
beforeEach(() => {
// Mocks for dependencies of TargetComponent.
return TestBed.configureTestingModule({
declarations: [TargetComponent],
providers: [
MockProvider(CONFIG),
MockProvider(CurrentUserService, {
firstName: 'firstName',
lastName: 'lastName',
}),
],
}).compileComponents();
});
it('covers first name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'first',
}),
);
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(fixture.componentInstance.name).toEqual('firstName');
});
it('covers last name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'last',
}),
);
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(fixture.componentInstance.name).toEqual('lastName');
});
Please, be sure you added MockInstance.scope(); before your tests.
It resets customizations of MockInstance after them.
Profit, with help of MockInstance,
you can customize any mock declarations in Angular test,
regardless whether they are InjectionToken, Service or even host Component or Directive.
Optimized version
If you want to reduce the amount of code in the example above,
you should use MockBuilder and MockRender.
MockInstance.scope();
beforeEach(() =>
MockBuilder(TargetComponent, ItsModule).mock(
CurrentUserService,
{
firstName: 'firstName',
lastName: 'lastName',
},
),
);
it('covers first name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'first',
}),
);
const fixture = MockRender(TargetComponent);
expect(fixture.point.componentInstance.name).toEqual(
'firstName',
);
});
it('covers last name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'last',
}),
);
const fixture = MockRender(TargetComponent);
expect(fixture.point.componentInstance.name).toEqual(
'lastName',
);
});
MockRender provides the component under fixture.point.componentInstance.
Live example
import {
Component,
Inject,
Injectable,
InjectionToken,
NgModule,
} from '@angular/core';
import {
MockBuilder,
MockInstance,
MockProvider,
MockRender,
} from 'ng-mocks';
import { TestBed } from '@angular/core/testing';
interface ConfigInterface {
displayName: 'first' | 'last';
}
const CONFIG = new InjectionToken<ConfigInterface>('CONFIG');
@Injectable()
class CurrentUserService {
firstName?: string;
lastName?: string;
}
@Component({
selector: 'target',
template: '{{ name }}',
})
class TargetComponent {
// A property which will be used somewhere else: in a template or wherever.
public name?: string;
// Required dependencies.
constructor(
@Inject(CONFIG) config: ConfigInterface,
user: CurrentUserService,
) {
// Business logic in the constructor to calculate the name.
if (config.displayName === 'first') {
this.name = user.firstName;
} else {
this.name = user.lastName;
}
}
TargetComponentMockInitializationLogic() {}
}
@NgModule({
declarations: [TargetComponent],
providers: [
{
provide: CONFIG,
useValue: {
displayName: 'first',
},
},
CurrentUserService,
],
})
class ItsModule {}
describe('MockInitializationLogic', () => {
describe('TestBed', () => {
// It is required if you cannot use default customizations.
// https://ng-mocks.sudo.eu/extra/install#default-customizations
// After each test it removes customizations which are done by MockInstance.
MockInstance.scope();
beforeEach(() => {
// Mocks for dependencies of TargetComponent.
return TestBed.configureTestingModule({
declarations: [TargetComponent],
providers: [
MockProvider(CONFIG),
MockProvider(CurrentUserService, {
firstName: 'firstName',
lastName: 'lastName',
}),
],
}).compileComponents();
});
it('covers first name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'first',
}),
);
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(fixture.componentInstance.name).toEqual('firstName');
});
it('covers last name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'last',
}),
);
const fixture = TestBed.createComponent(TargetComponent);
fixture.detectChanges();
expect(fixture.componentInstance.name).toEqual('lastName');
});
});
describe('MockBuilder', () => {
MockInstance.scope();
beforeEach(() =>
MockBuilder(TargetComponent, ItsModule).mock(
CurrentUserService,
{
firstName: 'firstName',
lastName: 'lastName',
},
),
);
it('covers first name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'first',
}),
);
const fixture = MockRender(TargetComponent);
expect(fixture.point.componentInstance.name).toEqual(
'firstName',
);
});
it('covers last name', () => {
// Customization for the use case.
MockInstance(
CONFIG,
(): ConfigInterface => ({
displayName: 'last',
}),
);
const fixture = MockRender(TargetComponent);
expect(fixture.point.componentInstance.name).toEqual(
'lastName',
);
});
});
});