If you prefer the kind of tests that minimize mocking as much as possible, you will be pretty happy with Standalone Components. Gone are the struggles of meticulously picking dependencies from NgModules for your Component under test.
Standalone Components come self-contained. Add them to your TestingModule's imports
property, and all their "visual elements" – Components, Directives, Pipes, and dependencies – become part of the test. As a nice side-effect, you reach a much higher code coverage.
If you are more of a visual learner, here's a video for you:
A Huge Dependency Graph
When we write a test, we check what Services our Component requires. Typical candidates are HttpClient
, ActivatedRoute
. We need to mock them. That's doable.
Unfortunately, the Components' dependencies also require Services, which - some of them - we also have to provide.
Consider the example of testing the RequestInfoComponent
. It contains the following dependencies:
A considerable number of Services derive from RequestInfoHolidayCardComponent
. That Subcomponent uses NgRx, which can be a heavy dependency on its own.
Looking at the necessary setup of the TestingModule
, there is quite a lot to consider:
const fixture = TestBed.configureTestingModule({
imports: [RequestInfoComponent],
providers: [
provideNoopAnimations(),
{
provide: HttpClient,
useValue: {
get: (url: string) => {
if (url === '/holiday') {
return of([createHoliday()]);
}
return of([true]).pipe(delay(125));
},
},
},
{
provide: ActivatedRoute,
useValue: {
paramMap: of({ get: () => 1 }),
},
},
provideStore({}),
provideState(holidaysFeature),
provideEffects([HolidaysEffects]),
{
provide: Configuration,
useValue: { baseUrl: 'https://somewhere.com' },
},
],
}).createComponent(RequestInfoComponent);
Mocking a Component
To improve the situation and still have an impactful test, we only want to mock the RequestInfoHolidayCard
. That would free us from quite a lot of Service dependencies:
Third-party libraries, like ng-mocks, provide functions to automate that. We do it manually to understand what's going on under the hood.
We add the code of the mocked Component directly into the test file.
@Component({
selector: 'app-request-info-holiday-card',
template: ``,
standalone: true,
})
class MockedRequestInfoHolidayCard {}
MockedRequestInfoHolidayCard
is a simple Component without any dependencies. What it has in common with the original is the selector. So when Angular sees the tag <app-request-info-holiday-card>
, it uses the mocked version.
The next step is to import the mock into the TestingModule
. With all its dependencies gone, the TestingModule
setup slims down quite a bit:
const fixture = TestBed.configureTestingModule({
imports: [RequestInfoComponent, MockedRequestInfoHolidayCard],
providers: [
provideNoopAnimations(),
{
provide: HttpClient,
useValue: {
get: (url: string) => of([true]).pipe(delay(125))
},
}
],
}).createComponent(RequestInfoComponent);
Unfortunately, that does not work. The test fails because ActivatedRoute
(dependency of RequestInfoHolidayCard
) is unavailable.
The reason should be clear. RequestInfoHolidayCard
is not part of the imports property of some NgModule
but directly of the RequestInfoComponent
. Although the mocked version is now part of the TestingModule
, the imports from RequestInfoComponent
internally override it.
We need to find an alternative solution.
TestBed::overrideComponent
Our only chance is to access the imports
property of the Component itself. Luckily, there is TestBed::overrideComponent()
.
A method that perfectly fits our use case. After overriding the imports
property of RequestInfoHolidayCard
, we configure the TestingModule
and proceed with the actual test.
TestBed.overrideComponent(RequestInfoComponent, {
remove: { imports: [RequestInfoComponentHolidayCard] },
add: { imports: [MockedRequestInfoHolidayCard] },
});
const fixture = TestBed.configureTestingModule({
imports: [RequestInfoComponent],
providers: [
provideNoopAnimations(),
{
provide: HttpClient,
useValue: {
get: (url: string) => of([true]).pipe(delay(125)),
},
},
],
}).createComponent(RequestInfoComponent);
Et voilà, that's much better!
A set
instead of add
or remove
method would override the complete imports
, providers
, etc.
Again: Please note that I highly recommend using ng-mocks. Mocking Components, Pipes, and Directives with it is way more comfortable.
Summary
Component tests, which include dependencies, give us higher code coverage, and we are also closer to the actual behavior. At the same time, the setup becomes harder.
Partial mocking is a good compromise. With Standalone Components, we must add the mock via TestBed::overrideComponent
.
There is also a TestBed::overrideDirective
and TestBed::overridePipe
for Directives or Pipes.
You can access the repository at https://github.com/rainerhahnekamp/how-do-i-test
If you encounter a testing challenge you'd like me to address here, please get in touch with me!
For additional updates, connect with me on LinkedIn, X, and explore our website for workshops and consulting services on testing.
https://www.angulararchitects.io/en/training/professional-angular-testing-playwright-edition/