If you favor a testing style that keeps mocking to a minimum, Standalone Components will likely make you quite happy. The old pain of carefully curating dependencies from NgModules for the component under test disappears. Standalone Components arrive self-contained. Once you add them to the imports of your TestingModule, every "visual element" — Components, Directives, Pipes, and their dependencies — is included in the test. A welcome side effect is that you achieve significantly higher code coverage.

For those who learn best by watching, there's a video available:


A Complex Network of Dependencies

When setting up a test, the first step is to identify which Services the Component relies on. Common examples include HttpClient and ActivatedRoute. These are typically mocked, which is straightforward enough.

The complication arises because the dependencies of your Component often have their own dependencies, and some of those also need to be provided in the test setup.

Take, for instance, testing RequestInfoComponent. It has the following dependencies:

Dependency graph of RequestInfoComponent

A significant number of these Services stem from RequestInfoHolidayCardComponent. This subcomponent relies on NgRx, which introduces its own heavyweight dependencies.

Looking at what it takes to configure the TestingModule for this scenario reveals a substantial amount of necessary boilerplate:

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 streamline this situation while maintaining a test that provides valuable feedback, the goal is to mock only the RequestInfoHolidayCard. Doing so removes the need to provide a wide array of Services:

RequestInfoComponent with mocked sub Component

Libraries designed for this, such as ng-mocks, offer functions to automate this process. For the sake of understanding the underlying mechanics, we will do it manually.

The code for the mocked Component is added directly within 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 with no dependencies. Its only shared trait with the original is the selector. This means when Angular encounters the tag <app-request-info-holiday-card>, it will render the mocked version instead.

The next step is to import this mock into the TestingModule. With all the ancillary dependencies removed, the TestingModule configuration becomes much more concise:


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

Regrettably, this approach fails. The test throws an error because ActivatedRoute, a dependency of RequestInfoHolidayCard, cannot be found.

The explanation is straightforward. RequestInfoHolidayCard is not declared in the imports of an NgModule; instead, it's directly imported by the RequestInfoComponent. Even though the mocked version is now included in the TestingModule, the imports defined within RequestInfoComponent take precedence and override it internally.

A different strategy is required.

TestBed::overrideComponent

The only way forward is to directly modify the imports property of the Component itself. Fortunately, TestBed::overrideComponent() exists for this exact purpose.

This method fits the use case perfectly. By overriding the imports of RequestInfoHolidayCard, we can then configure the TestingModule and run 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

And just like that, the test setup is far more manageable.

A set operation, rather than add or remove, is used to replace the entire imports array, along with providers and other metadata.

Once more, I strongly suggest using ng-mocks for this kind of work. Mocking Components, Pipes, and Directives with that library is significantly more convenient.

Wrapping Up

Tests that incorporate real dependencies yield higher code coverage and better reflect actual runtime behavior. However, this comes at the cost of a more complex testing setup.

Selective mocking offers a robust middle ground. For Standalone Components, the mock needs to be introduced via TestBed::overrideComponent, as demonstrated.

Analogous methods, TestBed::overrideDirective and TestBed::overridePipe, serve the same purpose for Directives and Pipes, respectively.


The repository can be found at https://github.com/rainerhahnekamp/how-do-i-test

If you have a testing conundrum you'd like me to cover, please don't hesitate to reach out.

For more updates, you can connect with me on LinkedIn or X. Be sure to check out our website for workshops and consulting services dedicated to testing.