How do I test and mock Standalone Components?

rainerhahnekamp

Rainer Hahnekamp

Posted on March 4, 2024

How do I test and mock Standalone Components?

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:

Dependency graph of RequestInfoComponent

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);


Enter fullscreen mode Exit fullscreen mode

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:

RequestInfoComponent with mocked sub Component

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.



Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
rainerhahnekamp
Rainer Hahnekamp

Posted on March 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related