bniedermeyer
bniedermeyer

Reputation: 1368

Unit testing Angular service that depends on another service that uses InjectionToken

I am working on unit testing an Angular service that looks like so:

/* data.service.ts */
import { Injectable } from '@angular/core';

import { FooService } from './foo.service';
import { Data } from './data.model';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  constructor(private fooService: FooService) {}

  addData(data: Data) {
    return this.fooService.getActiveFoo().addData(data);
  }
}

FooService is a service in the same module. It injects some values using InjectionTokens.

/* foo.service.ts */
import { Inject, Injectable } from '@angular/core';

import { KEY1, KEY2 } from './tokens';
import { Foo } from './foo.ts';

@Injectable({
  providedIn: 'root'
})
export class FooService{

  activeFoo: Foo; 

  constructor(@Inject(KEY1) private key1: string, @Inject(KEY2) private key2: string) {}

  ...

  getActiveFoo() {
    return this.activeFoo; 
 }
}


/* tokens.ts */
import { InjectionToken } from '@angular/core';

export const KEY1 = new InjectionToken('KEY_1');
export const KEY2 = new InjectionToken('KEY_2');

All three files are part of the same module, which is set up as so:

/* bar.module.ts */
import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { KEY1, KEY2 } from './tokens';
import { DataService } from './data.service';
import { FooService } from './foo.service';

@NgModule({
  imports: [CommonModule],
})
export class BarModule {
  static forRoot(key1Val: string, key2Val: string): ModuleWithProviders {
    return {
      ngModule: BarModule,
      providers: [
        DataService,
        FooService,
        { provide: KEY1, useValue: key1Val},
        { provide: KEY2, useValue: key2Val}
      ]
    };
  }
}

This is working fine in the app itself. Now I am working on writing a unit test for data.service.ts and am running into issues with the provided InjectionTokens.

/* data.service.spec.ts */
import { TestBed, inject } from '@angular/core/testing';

import { DataService } from './data.service';
import { FooService } from './foo.service';
import { KEY1, KEY2 } from './tokens';

describe('DataService', () => {
  const mockFooService = jasmine.createSpyObj(['getActiveFoo']);

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        DataService,
        { provide: FooService, useValue: mockFooService },
        { provide: KEY1, useValue: 'abc' },
        { provide: KEY2, useValue: '123' }
      ]
    });
  });

  it('should be created', inject([DataService], (service: DataService) => {
    expect(service).toBeTruthy();
  }));
});

When I run my tests I am receiving an error saying

Error: StaticInjectorError(DynamicTestModule)[FooService -> InjectionToken KEY_1]: 
  StaticInjectorError(Platform: core)[FooService -> InjectionToken KEY_1]: 
    NullInjectorError: No provider for InjectionToken KEY_1!

I have tried using empty objects instead of strings in the test providers but still get the same error. Is there another way I need to be providing these tokens?

Upvotes: 1

Views: 2643

Answers (1)

bniedermeyer
bniedermeyer

Reputation: 1368

I solved this by adding factory functions to the InjectionTokens that returns a default value for the token. When this is done it defaults to providing the tokens at the root injector.

Since both services were being provided by the root injector and the tokens were being provided in the BarModule's injector they did not have visibility to each other in the test bed, but did when the entire module was loaded by my app since they were all present in the module. I confirmed this by removing providedIn from the two services and leaving the tokens as they were originally.

My tokens now look like

/* tokens.ts */
import { InjectionToken } from '@angular/core';

export const KEY1 = new InjectionToken<string>('KEY_1', {factory: () => '' });
export const KEY2 = new InjectionToken<string>('KEY_2', {factory: () => '' });

As an added benefit, the tokens are now tree-shakeable. See here for more information.

Upvotes: 1

Related Questions