How to test a standalone component in Angular and mock its `imports`
This section describes how to test a standalone component.
Usually, developers want to mock all dependencies.
For a standalone component, it means all its imports.
This behavior is possible to achieve with MockBuilder.
Let's imagine we have the following standalone component:
@Component({
selector: 'target',
template: `<dependency>{{ name | standalone }}</dependency>`,
standalone: true,
imports: [DependencyModule, StandalonePipe],
})
class StandaloneComponent {
@Input() public readonly name: string | null = null;
}
It imports DependencyModule, which provides DependencyComponent,
and StandalonePipe, and ideally both should be mocked.
The answer is:
beforeEach(() => {
return MockBuilder(StandaloneComponent);
});
Under the hood it marks StandaloneComponent as kept
and sets shallow and export flags:
beforeEach(() => {
return MockBuilder().keep(StandaloneComponent, {
shallow: true,
export: true,
});
});
That's it. Now all imports of StandaloneComponent are mocks,
and its properties, methods, injections and template are available for testing.
If you need to keep an import, simply call .keep with it.
For example, if we wanted to keep StandalonePipe, the code would look like this:
beforeEach(() => {
return MockBuilder(StandaloneComponent).keep(StandalonePipe);
});
Circular standalone imports on Angular 15
Angular 15.0.0 through 15.2.3 cannot configure TestBed when standalone components form a circular import graph.
For example, the error occurs when a child imports its parent with forwardRef, while the parent imports the child directly:
@Component({
standalone: true,
imports: [forwardRef(() => ParentComponent)],
})
class ChildComponent {}
@Component({
standalone: true,
imports: [ChildComponent],
})
class ParentComponent {}
In those Angular versions, TestBed traverses the imports recursively before ng-mocks overrides can be applied and throws RangeError: Maximum call stack size exceeded.
Angular fixed the traversal in 15.2.4.
Upgrade all @angular/* packages to 15.2.4 or a newer matching patch version before testing circular standalone imports.
See Angular issue #49469 and ng-mocks issue #6143 for the original reports.
Live example
import {
Component,
Input,
NgModule,
Pipe,
PipeTransform,
} from '@angular/core';
import { MockBuilder, MockRender, ngMocks } from 'ng-mocks';
// A simple standalone pipe we are going to mock.
@Pipe({
name: 'standalone',
standalone: true,
})
class StandalonePipe implements PipeTransform {
transform(value: string | null): string {
return `${value}:${this.constructor.name}`;
}
}
// A simple dependency component we are going to mock.
@Component({
selector: 'dependency-standalone-component',
template: '<ng-content></ng-content>',
})
class DependencyComponent {
@Input() public readonly name: string | null = null;
}
// A module which declares and exports the dependency component.
@NgModule({
declarations: [DependencyComponent],
exports: [DependencyComponent],
})
class DependencyModule {}
// A standalone component we are going to test.
@Component({
selector: 'standalone',
template: `<dependency-standalone-component [name]="name">{{
name | standalone
}}</dependency-standalone-component>`,
standalone: true,
imports: [
DependencyModule,
StandalonePipe,
],
})
class StandaloneComponent {
@Input() public readonly name: string | null = null;
}
describe('TestStandaloneComponent', () => {
beforeEach(() => {
return MockBuilder(StandaloneComponent);
});
it('renders dependencies', () => {
const fixture = MockRender(StandaloneComponent, {
name: 'test',
});
// asserting that we passed the input
const dependencyComponent = ngMocks.findInstance(
DependencyComponent,
);
expect(dependencyComponent.name).toEqual('test');
// asserting how we called the pipe
const standalonePipe = ngMocks.findInstance(StandalonePipe);
// it's possible because of autoSpy.
expect(standalonePipe.transform).toHaveBeenCalledWith('test');
// or asserting virtual DOM
expect(
ngMocks.input(
ngMocks.find(fixture, DependencyComponent),
'name',
),
).toEqual('test');
});
});