Rainer Hahnekamp
Posted on March 4, 2024
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:
```typescript
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.
```typescript
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](https://ng-mocks.sudo.eu/). 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](https://www.linkedin.com/in/rainerhahnekamp), [X](https://twitter.com/rainerhahnekamp), and explore our website for workshops and consulting services on testing.
Posted on March 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.