Search code examples
angulardependency-injectionjasminei18next

Jasmine (unit) test Angular component that uses i18next (translation) functionality - getting "No provider for InjectionToken I18NEXT_SERVICE"


I'm trying to test through Jasmine (version 3.99) an Angular (version 9) component that utilizes i18next for its translations. Note that the code for the component renders as desired when viewed through our application, however in the Jasmine test outlined below it does not (the full error message I received is listed near the bottom of the post). Also note that I'm not wanting to mockup the translations or any i18next functionality - i.e. I want my component to use/render the translations as normal. The component setup is as follows

constructor(@Inject(I18NEXT_SERVICE) private i18NextService: ITranslationService,
  private myTranslationTextService: MyTranslationTextService
) {
  ...
}
public ngOnInit() {
  const enTranslations = this.myTranslationTextService.getEnTranslations(); <--get translations in JSON
  i18next
    .init({
      supportedLngs: ['en',...],
      fallbackLng: 'en',
      debug: true,
      returnEmptyString: true,
      ns: [
        'translation',
      ],
      resources: {
        en: {
          translation: enTranslations
        },
        ... //other translations
      },
      interpolation: {
        format: I18NextModule.interpolationFormat(defaultInterpolationFormat),
      },
    })
    .then(() => {
      this.getData(); //<--call i18NextService methods and gets core data for MyComponent's template
    })
    .catch(err => {
      console.log(err);
    });
}
getData() {
  this.i18NextService.changeLanguage('en'); //<--calls method within i18NextService
  ...
}

My spec looks like the following:-

export function appInit(i18next: ITranslationService) {
  //return () => i18next.init();
  return () => {
    let promise: Promise<I18NextLoadResult> = i18next.init({
      lng: 'cimode',
    });
    return promise;
  };
}

export function localeIdFactory(i18next: ITranslationService) {
  return i18next.language;
}

export const I18N_PROVIDERS = [
  {
    provide: APP_INITIALIZER,
    useFactory: appInit,
    deps: [I18NEXT_SERVICE],
    multi: true
  },
  {
    provide: LOCALE_ID,
    deps: [I18NEXT_SERVICE],
    useFactory: localeIdFactory
  },
];

describe('My component', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let mock: MyMockDataService = new MyMockDataService();
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent],
      imports: [I18NextModule.forRoot()],
      providers: [
        //I18NEXT_SERVICE,
        { provide: I18NEXT_SERVICE },
        //{ provide: I18NEXT_SERVICE, useValue: {} as ITranslationService },
        //{ provide: I18NEXT_SERVICE, useValue: TestBed.get(I18NEXT_SERVICE) },
        I18N_PROVIDERS,
        MyTranslationTextService
      ],
      schemas: [NO_ERRORS_SCHEMA]
    })
    .compileComponents();

    mockMyTranslationTextService = TestBed.inject(MyTranslationTextService) as jasmine.SpyObj<MyTranslationTextService>;
    spyOn(mockMyTranslationTextService, 'getEnTranslations').and.returnValue(mock.getEnTranslations());
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(ParentMStep2022Component);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should render correctly', () => {
    expect(component).toBeTruthy();
  })
});

MyMockDataService is simply:-

export class MyMockDataService {
  getEnTranslations() {
    return of(
      [
        {
          "text1": "text1 EN phrase",
          "text2": "text2 EN phrase",
          ...
        }
      ]
    );
  }
}

However after trying a number of different options in my test - eg.

  • adding I18NEXT_SERVICE to the providers list
  • adding I18NEXT_SERVICE to the providers list with a initial instance - as in "{ provide: I18NEXT_SERVICE, useValue: {} }")
  • as mentioned I want to use the library in the unit tests (while testing other behaviors such as how it renders, etc) so mocking I18NEXT_SERVICE isn't really an option
  • https://github.com/Romanchuk/angular-i18next/issues/12 - this refers to a change an early version of angular-i18next (I'm using version 10)
  • https://angular.io/errors/NG0201 - the I18NEXT_SERVICE token is injectable (InjectionToken)
  • upgrading jasmine to the latest version (4.4.0)

...I am getting:-

NullInjectorError: R3InjectorError(DynamicTestModule)[InjectionToken I18NEXT_SERVICE -> InjectionToken I18NEXT_SERVICE]:
NullInjectorError: No provider for InjectionToken I18NEXT_SERVICE!

To setup i18next I've followed https://github.com/Romanchuk/angular-i18next/blob/master/README.md - it refers to the test project at https://github.com/Romanchuk/angular-i18next/tree/master/libs/angular-i18next/src/tests/projectTests (note however the test project doesn't inject the i18NextService token in the constructor - injecting is the recommendation)

Can anyone shed some light ?


Solution

  • Thanks to @satanTime I had the idea of add the tick() code as follows to proactively run the already queued tasks

    describe('My component', () => {
      let component: MyComponent;
      let fixture: ComponentFixture<MyComponent>;
      let mock: MyMockDataService = new MyMockDataService();
      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [MyComponent],
          imports: [I18NextModule.forRoot()],
          providers: [
            //I18NEXT_SERVICE,
            //{ provide: I18NEXT_SERVICE },
            //{ provide: I18NEXT_SERVICE, useValue: {} as ITranslationService },
            //{ provide: I18NEXT_SERVICE, useValue: TestBed.get(I18NEXT_SERVICE) },
            I18N_PROVIDERS,
            MyTranslationTextService
          ],
          schemas: [NO_ERRORS_SCHEMA]
        })
          .compileComponents();
    
        mockMyTranslationTextService = TestBed.inject(MyTranslationTextService) as jasmine.SpyObj<MyTranslationTextService>;
        spyOn(mockMyTranslationTextService, 'getEnTranslations').and.returnValue(mock.getEnTranslations());
      }));
    
      beforeEach(() => {
        fixture = TestBed.createComponent(ParentMStep2022Component);
        component = fixture.componentInstance;
        //fixture.detectChanges();
      });
    
      it('should render correctly', fakeAsync(() => {
        fixture.detectChanges();
        tick();
        fixture.detectChanges();
        tick();
        fixture.detectChanges();
        expect(component).toBeTruthy();
      }));
    });