4

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.

Good Night Nerd Pride
  • 8,245
  • 4
  • 49
  • 65

2 Answers2

2

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.

kvetis
  • 6,682
  • 1
  • 28
  • 48
  • Thanks for the answer but this doesn't work. The error is `StaticInjectorError[InjectionToken foo token]: NullInjectorError: No provider for InjectionToken foo token!`. See this example: https://stackblitz.com/edit/angular-py8wbd (check the file `app.component.ts`). – Good Night Nerd Pride Nov 11 '19 at 10:31
  • 1
    @GoodNightNerdPride Updated answer. You need to use `TestBed` instead of `Injector`. – kvetis Nov 11 '19 at 10:59
0

I've discovered that the following works:

class Example {}

export const EXP = new InjectionToken<Example>(
  'EXP',
  {
    factory: (): Example => new Example(),
    providedIn: 'root',
  },
);

@Injectable({ providedIn: 'root' })
export class SomeService {
  constructor(@Inject(EXP) private readonly exp: Example) {}
}

  // @ts-expect-error StaticProvider type is missing a union for InjectionToken with factories 
  const injector = Injector.create({
    providers: [
      { provide: EXP },
      { provide: SomeService, useClass: SomeService, deps: [ EXP ] },
    ],
  });

  const theService = injector.get(SomeService);

But you have to silence the compiler error because the types in Angular are missing InjectionToken for the StaticProvider.

Alternatively you can do:

  const injector = Injector.create({
    providers: [
      [ { provide: EXP } ],
      { provide: SomeService, useClass: SomeService, deps: [ EXP ] },
    ],
  });

and take advantage of the any[] in that type to avoid the // @ts-expect-error.

It was pointed out to me that the full code for that example in the documentation is the spec file unit test. However it uses a magic MockRootScopeInjector which doesn't seem like production worthy code.

Rob
  • 3,687
  • 2
  • 32
  • 40
  • This is for Angular v14, which is rather old. But I think it will apply to Angular v15 also. – Rob Apr 12 '23 at 23:31