Motivation and quick start
Angular testing is fun and easy until you have met complex dependencies,
and setting up the TestBed
becomes really annoying and time-consuming.
ng-mocks
helps to bring fun and ease back allowing developers
to create mock child components
and stub dependencies via a few lines of code with help of
MockService
,
MockComponent
,
MockDirective
,
MockPipe
,
MockProvider
,
MockModule
,
or with pro tools such as
MockBuilder
with
MockRender
.
Let's suppose that in our Angular application you have a component, called AppBaseComponent
,
and its template looks like that:
<app-header [menu]="items">
<app-search (search)="search($event)" [results]="search$ | async">
<ng-template #item let-item>
<strong>{{ item }}</strong>
</ng-template>
</app-search>
{{ title | translate }}
</app-header>
<app-body appDark>
<router-outlet></router-outlet>
</app-body>
<app-footer></app-footer>
This means that the component depends on the next child components, services and declarations:
AppHeaderComponent
AppSearchComponent
AppBodyComponent
AppFooterComponent
SearchService
TranslatePipe
You could easily test it with schemas: [NO_ERRORS_SCHEMA]
to avoid
Template parse errors: <component> is not a known element
,
and it would work, but in this case you have zero guarantee, that our tests will fail
if an interface of a dependency has been changed and requires
code updates. Therefore, you have to avoid NO_ERRORS_SCHEMA
.
However, it forces us putting all dependencies in the TestBed
like that:
TestBed.configureTestingModule({
declarations: [
// The only declaration we care about.
AppBaseComponent,
// Dependencies.
AppHeaderComponent,
AppDarkDirective,
TranslatePipe,
// ...
],
imports: [
CommonModule,
AppSearchModule,
// ...
],
providers: [
SearchService,
// ...
],
});
And yes, nobody knows which dependencies the dependencies have, although we definitely know that you do not want to worry about them.
That is where ng-mocks
comes for help. Simply pass all the dependencies
into helper functions to get their mock versions
and to avoid a dependency hassle.
TestBed.configureTestingModule({
declarations: [
// The only declaration we care about.
AppBaseComponent,
// Mocking dependencies.
MockComponent(AppHeaderComponent),
MockDirective(AppDarkDirective),
MockPipe(TranslatePipe),
// ...
],
imports: [
MockModule(CommonModule),
MockModule(AppSearchModule),
// ...
],
providers: [
MockProvider(SearchService),
// ...
],
});
If you have noticed search$ | async
in the template, you made the right assumption:
you need to provide a fake observable stream to avoid failures
like Cannot read property 'pipe' of undefined
,
when the component tries to execute this.search$ = this.searchService.result$.pipe(...)
in ngOnInit
.
For example, you can implement it via MockInstance
:
beforeEach(() =>
MockInstance(SearchService, () => ({
result$: EMPTY,
})),
);
or if you want to set that as default mock behavior for all tests,
you can use ngMocks.defaultMock
in src/test.ts
:
ngMocks.defaultMock(SearchService, () => ({
result$: EMPTY,
}));
Profit. Now, you can forget about noise of child dependencies.
Nevertheless, if we count lines of mock declarations,
we see that there are a lot of them,
and looks like here might be dozens more for components with many dependencies from many modules.
Also, what happens if someone has deleted AppSearchModule
from AppBaseModule
? Does not look like the test will fail due to
a missed dependency.
Right, we need a tool that would extract declarations of the module
AppBaseComponent
belongs to, and create mocks out of them like the code above.
Then, if someone deletes AppSearchModule
the test fails too.
ngMocks.guts
and MockBuilder
are the tool for that.
ngMocks.guts
ngMocks.guts
works like that: it accepts 3 parameters, each one is optional.
- 1st parameter accepts things we want to test as they are, these won't be mocked.
- 2nd parameter accepts dependencies of the things. These dependencies will be mocked. In the case of modules, their imports, declarations and providers (guts) will be mocked.
- 3rd parameter accepts things which should be excluded from the dependencies to provide sophisticated mocks later.
Any parameter can be null
to neglect it, or an array if we want to pass more than one thing.
Now, let's apply ngMocks.guts
to AppBaseComponent
and its AppBaseModule
from the beginning of this article.
The goal is to mock guts of AppBaseModule
, but to keep AppBaseComponent
as it is for testing,
and to replace SearchService
with a sophisticated mock.
Therefore,
AppBaseComponent
should be passed as the first parameter,
AppBaseModule
as the second one,
and SearchService
as the third one.
const testModuleDeclaration = ngMocks.guts(
AppBaseComponent, // keep
AppBaseModule, // mock
[SearchService], // exclude
);
ngMocks.guts
detects that AppBaseModule
is a module and extracts its guts
respecting the 1st and the 3rd parameters, what should be mocked and excluded.
The result of ngMocks.guts
is the same as:
const testModuleDeclaration = {
declarations: [
AppBaseComponent, // keep
MockComponent(AppHeaderComponent),
MockDirective(AppDarkDirective),
MockPipe(TranslatePipe),
],
imports: [
MockModule(CommonModule),
MockModule(AppSearchModule),
],
providers: [
// SearchService, // exclude
],
};
Now, let's add a sophisticated mock for SearchService
.
testModuleDeclaration.providers.push({
provide: SearchService,
useValue: SophisticatedMockSearchService,
});
Profit. TestBed
can be configured now.
TestBed.configureTestingModule(testModuleDeclaration);
And all together:
beforeEach(() => {
const testModuleDeclaration = ngMocks.guts(
AppBaseComponent, // keep
AppBaseModule, // mock
[SearchService], // exclude
);
testModuleDeclaration.providers.push({
provide: SearchService,
useValue: SophisticatedMockSearchService,
});
return TestBed.configureTestingModule(testModuleDeclaration);
});
Lazy loaded modules
What about lazy loaded modules?
If you have a lazy module, then it alone might be not sufficient, and
you need to add its parent module, for example AppModule
.
In such a case, simply pass an array of modules as the second
parameter.
TestBed.configureTestingModule(
ngMocks.guts(
AppBaseComponent, // keep
[AppBaseModule, AppModule], // mock
),
);
Profit. That should be enough for the start.
MockBuilder
The functions above help with an easy start, but they do not cover all
possible cases and do not provide tools for customizing behavior.
Consider reading MockBuilder
and MockRender
if you want to create mocks for child dependencies like a pro
in Angular tests.
For example, if you needed TranslatePipe
to prefix its strings instead of
translating them, and to create a stub SearchService
with an empty result which would not cause
an error during execution because of a missed observable in its mock object,
the code would look like:
beforeEach(() => {
return MockBuilder(AppBaseComponent, AppBaseModule)
// TranslatePipe is declarared / imported in AppBaseModule
.mock(TranslatePipe, v => `translated:${v}`)
// SearchService is provided / imported in AppBaseModule
.mock(SearchService, {
result$: EMPTY,
});
});
Profit.
Have a question still? Do not hesitate to contact us.