How to globally register Injection Tokens within the Angular TestBed

profile
Tim Deschryver
timdeschryver.dev

It's a useful practice To keep your test setups simple (and DRY). However, this can become a problem with Injection Tokens that are provided at a global level (the application level, or a feature level), and are used in many layers of the application. In this blog post, you will see how to configure the Angular TestBed at a global level in order to prevent this problem.

Think of a typical injection token that you want to register once at the global level. This is usually a token that contains information that doesn't affect the behavior of an application but contains useful information for the application to work correctly. Examples of such tokens are tokens that contain environment variables, the theme of the application, ... Most of these tokens don't have any value for the test cases themselves but need to be registered so Angular can create instances of components and services that depend on these tokens.

Tokens that are registered at the component level are automatically taken care of (with Angular Standalone Components), so we don't have to worry about them anymore.

The problem link

When a token is not registered within the TestBed, but is injected in a component or service, the NullInjectorError error is thrown:

To resolve this error, the injection token MY_INJECTION_TOKEN needs to be registered within the TestBed. With the Angular Testing Library this is done by providing the token in the providers array of the render method:

While this works fine, we don't want to repeat this for every test setup. Instead, we want to register the token once at the global level of the TestBed.

It also makes sense to register these tokens globally within the test suite, because they're provided at the global level of the application. Injection tokens that are provided at the component level are automatically taken care of when using Angular Standalone Components.

The beforeEach solution link

Registering a token globally is done by adding the token to the global test setup file:

Using TestBed.overrideProvider it's possible to set a value for the injection token. Because we want to set the value once for all tests, we need to wrap this in a beforeEach hook:

A drawback of this approach is that the value of the token is set for all tests, and it's less intuitive to override this value for a specific test. For those one-off cases, we need to use the TestBed.overrideProvider method again. This isn't ideal, because we need to know which token is registered globally, and the TestBed.overrideABC() methods are not so common knowledge. The ideal solution is to set the injection token for those one-off cases the same way as we did before, with the providers array when we're busy configuring the TestBed.o

To overcome this, we need to tackle this problem from another angle.

The initTestEnvironment solution link

Instead of overriding the injection token, we need to provide the injection token in the initial setup of the test environment. For this, we use the platformBrowserDynamicTesting() method, which accepts an array of providers.

For Karma users, this is simple and we can make a small change to the test.ts file:

For Jest users, this is a bit more complicated because the jest-preset-angular preset already initializes the test environment. Sadly, we can't alter this configuration, nor can we invoke the initialization of the test environment again (this throws an error). The workaround is to move the whole initialization to our end, within the setup-test.ts file:

In code, this translates that we don't import the pre-configured setup jest-preset-angular/setup-jest anymore.

So instead of doing the above, we configure the test environment ourselves. This looks a lot like the setup that's used with Karma.

Important here is to not forget to import the zone-testing-bundle file to make sure that zone.js is loaded. Lastly, we can add the injection token to the platformBrowserDynamicTesting() method.

Conclusion link

Using the platformBrowserDynamicTesting() method while initializing the test environment, it's possible to register injection tokens globally. This solution is more intuitive than using the TestBed.overrideProvider() method, and it also allows us to override the value of the injection token for a specific test when we need to set an explicit value for the token.

For services that are provided at the root level, that you want to mock, you can resort to the TestBed.overrideProvider() method. This can be useful for services that don't have any business logic but only consists of application logic. These services don't change the behavior of the application, e.g. a logging service.

Feel free to update this blog post on GitHub, thanks in advance!

Join My Newsletter (WIP)

Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.

Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo

Share this post

Twitter LinkedIn