Search code examples
angularangular-dependency-injection

How to provide an `InjectionToken` that has its own `factory`?


Consider the following InjectionToken for the type Foo:

export const FOO = new InjectionToken<Foo>(
  'foo token',
  { factory: () => new Foo() });

Now assume I was crazy enough to aim for 100% test coverage. To that end I'd have to unit test that little factory function.

I was thinking to create an injector that has just one provider in my test:

const inj = Injector.create({
  providers: [{ provide: FOO }] // compiler error here
});

const foo = inj.get(FOO);

expect(foo).toBeTruthy();

Unfortunately this fails with a compiler error, because { provide: FOO } is not a valid provider without a useValue, useFactory, or useExisting property. But why am I forced to define one of them when the injection token comes with its own factory?

Of course I tried all options nonetheless:

  • useValue: FOO compiles and runs, but doesn't seem to execute the factory method
  • useFactory: () => FOO, deps: [] also compiles and runs, but doesn't seem to execute the factory method either
  • useExisting: FOO compiles, but fails with a circular dependency error during runtime

Funny enough, a similar scenario is presented in the documentation for InjectionToken, but it doesn't show the registration I'm looking for:

const MY_SERVICE_TOKEN = new InjectionToken<MyService>('Manually constructed MyService', {
  providedIn: 'root',
  factory: () => new MyService(inject(MyDep)),
});

// How is `MY_SERVICE_TOKEN` token provided?

const instance = injector.get(MY_SERVICE_TOKEN);

I created an example on StackBlitz so you can try yourself.


Solution

  • When you specify factory function for the InjectionToken, the token is automatically provided in root. Therefore you don't need to provide it in the test bed either.

    In order to use this feature in test, you need to use TestBed instead of just Injector.create.

    import { TestBed } from '@angular/core/testing';
    
    describe('Foo', () => {
      beforeEach(() => TestBed.configureTestingModule({}));
    
      it('should be created', () => {
        const service: Foo = TestBed.get(FOO);
        expect(service).toBeTruthy();
      });
    });
    

    The docs say

    When creating an InjectionToken, you can optionally specify a factory function which returns (possibly by creating) a default value of the parameterized type T. This sets up the InjectionToken using this factory as a provider as if it was defined explicitly in the application's root injector. If the factory function, which takes zero arguments, needs to inject dependencies, it can do so using the inject function. See below for an example.