DEV Community

Cover image for How do I test and mock Standalone Components?
Rainer Hahnekamp for This is Angular

Posted on • Originally published at rainerhahnekamp.com

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 {}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (2)

Collapse
 
jangelodev profile image
João Angelo

Hi Rainer Hahnekamp,
Your tips are very useful
Thanks for sharing

Collapse
 
rainerhahnekamp profile image
Rainer Hahnekamp

You are very welcome João!