angulardependency-injectionjestjsnrwl-nxnx-monorepo

NX Angular unit tests fail because of NullInjectorError


I'm trying to use Angular Injection Tokens in my app, but it keeps causing my unit tests to fail.

Here is a repo that demonstrates the issue:

https://github.com/shadow1349/nx-filereplacementissue/tree/main

If you check out the main branch, run npm install, and then run npx nx run test:test You'll see the following error:

NullInjectorError: R3InjectorError(Standalone[ComponentsComponent])
[InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN]:
NullInjectorError: No provider for InjectionToken TEST_INJECTION_TOKEN!

Here's my injection token located here: https://github.com/shadow1349/nx-filereplacementissue/blob/main/shared/injection-tokens/src/index.ts

export const TEST_INJECTION_TOKEN = new InjectionToken<BehaviorSubject<string>>(
  'TEST_INJECTION_TOKEN'
);

My app config, located here: https://github.com/shadow1349/nx-filereplacementissue/blob/main/test/src/app/app.config.ts

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { appRoutes } from './app.routes';
import { TEST_INJECTION_TOKEN } from '@test-filereplacements/injection-tokens';
import { BehaviorSubject } from 'rxjs';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes),
    {
      provide: TEST_INJECTION_TOKEN,
      useValue: new BehaviorSubject<string>('Hello World'),
    },
  ],
};

Then I have a shared component that's imported into my App Component called ComponentsComponent, located here:

https://github.com/shadow1349/nx-filereplacementissue/blob/main/test/src/app/app.component.ts

and here:

https://github.com/shadow1349/nx-filereplacementissue/blob/main/shared/components/src/lib/components/components.component.ts

// app component
import { Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NxWelcomeComponent } from './nx-welcome.component';
import { ComponentsComponent } from '@test-filereplacements/components';

@Component({
  standalone: true,
  imports: [NxWelcomeComponent, RouterModule, ComponentsComponent],
  selector: 'test-filereplacements-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {}

//components.component.ts
import { Component, Inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TEST_INJECTION_TOKEN } from '@test-filereplacements/injection-tokens';
import { BehaviorSubject } from 'rxjs';

@Component({
  selector: 'test-filereplacements-components',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './components.component.html',
  styleUrl: './components.component.css',
})
export class ComponentsComponent {
  constructor(
    @Inject(TEST_INJECTION_TOKEN) public testToken: BehaviorSubject<string>
  ) {
    console.log('testToken', this.testToken);
  }
}

This all works fine when you run npx nx run test:serve --configuration=development it will console log just fine.

Here's where you will run into the issue. If you run the test with Jest it will pass. However, when you run npx nx run test:test it fails with:

NullInjectorError: R3InjectorError(Standalone[ComponentsComponent])
[InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN]:
NullInjectorError: No provider for InjectionToken TEST_INJECTION_TOKEN!

I know the first question you're going to ask is if in app.component.ts do I have the provider for the injection token, yes I do. You can see it here:

https://github.com/shadow1349/nx-filereplacementissue/blob/main/test/src/app/app.component.spec.ts

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
import { RouterTestingModule } from '@angular/router/testing';
import { TEST_INJECTION_TOKEN } from '@test-filereplacements/injection-tokens';
import { BehaviorSubject } from 'rxjs';

describe('AppComponent', () => {
  let component: AppComponent;
  let fixture: ComponentFixture<AppComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent, NxWelcomeComponent, RouterTestingModule],
      providers: [
        {
          provide: TEST_INJECTION_TOKEN,
          useValue: new BehaviorSubject<string>('Hello World'),
        },
      ],
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

Yet, it still thinks that the injection token doesn't exist. Does anyone know why?

Thank you!


Solution

  • NullInjectorError: R3InjectorError(Standalone[ComponentsComponent])
    [InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN -> InjectionToken TEST_INJECTION_TOKEN]:
    NullInjectorError: No provider for InjectionToken TEST_INJECTION_TOKEN!
    

    The inject token is provided in the constructor of ComponentsComponent.ts

    To fix this issue, you need to provide InjectionToken in your component :

    Approach with customer provider: create your customer provider and can be called at the root and feature levels

    export const provideToken = (value = 'TEST 123') => {
      const providers: Provider[] = [
        {
          provide: TEST_INJECTION_TOKEN,
          useValue: new BehaviorSubject<string>(value),
        }
      ];
      return providers;
    };
    

    ComponentsComponent with customer provider:

    @Component({
      selector: 'test-filereplacements-components',
      standalone: true,
      imports: [CommonModule],
      templateUrl: './components.component.html',
      styleUrl: './components.component.css',
      providers: [
        provideToken()
      ],
    })
    

    And app.config.ts

    export const appConfig: ApplicationConfig = {
      providers: [
        provideRouter(appRoutes),
        provideToken()
      ],
    };