Search code examples
angularunit-testingjasminekarma-runnerangular-standalone-components

Angular unit test failing setup with NullInjector error and not using Stub


I'm writing a simple standalone Logout component test using a service depending on HttpClient. I tried different solutions to just stub the HttpAuthService and every solution I tried so far all throw :

NullInjectorError: R3InjectorError(Standalone[LogoutComponent])[HttpAuthService -> HttpAuthService -> HttpClient -> HttpClient]: NullInjectorError: No provider for HttpClient!

The service I try to stub:

@Injectable({
  providedIn: 'root'
})
export class HttpAuthService implements AuthService {

  private userSignal = signal<User|null>(null);
  private backendURL = environment.backendURL;

  public user = computed<User | null>(this.userSignal);
  constructor(private httpClient: HttpClient) {
  }
  login(params: { email: string; password: string }): Observable<void> {
    return this.httpClient.get<any[]>(`${this.backendURL}/users?email=${params.email}`)
      .pipe(
        tap(result=> {
          if (result.length > 0) {
            this.userSignal.set({userName: result[0].email})
          }
        }),
        map( result => {
          if (result.length == 0) {
            throw new Error('User not found');
          } else {
            return undefined;
          }
        })
      )
  }
  logout(): void {
    this.userSignal.set(null);
  }
}

The Logout component:

@Component({
  selector: 'app-logout',
  standalone: true,
  imports: [MatButtonModule, MatIconModule],
  template: `
    <button mat-icon-button (click)="onLogout()" >
      <mat-icon>logout</mat-icon>
    </button>
  `,
  styles: ``
})
export class LogoutComponent {


  constructor(private authService: HttpAuthService) {
  }

  onLogout() {
    this.authService.logout();
  }
}

The Logout unittest:

describe('LogoutComponent', () => {
  let component: LogoutComponent;
  let fixture: ComponentFixture<LogoutComponent>;
  const fakeAuthService = new AuthStub();

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [LogoutComponent],
      providers: [{provide: HttpAuthService, useClass: AuthStub}]
    })
    .compileComponents();

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

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

export  class AuthStub implements AuthService {
  login(params: { email: string; password: string }): Observable<void> {
    return of(undefined);
  }
  logout(): void {
  }
}

Solution tried:

  • Using jasmine.createSpyObj('', {//login and logout defined}) and added to providers with {provide/useValue}
  • Using AuthStub as coded below, created a const with a new instance and and added to providers with {provide/useValue}
  • Using AuthStub as coded below, created a const with a new instance and and added to providers with {provide/useClass}

To rule out IDE (Webstorm) I also runned the test in terminal with ng test. Test setup is using default ng Karma/jasmine test setup with Playwright for e2e testing.


Solution

  • Could you try this code, I think since its standalone component, it needs to be instantiated differently!

    describe('LogoutComponent', () => {
      let component: LogoutComponent;
      let fixture: ComponentFixture<LogoutComponent>;
    
      beforeEach(async () => {
        fixture = await render(LogoutComponent, {
          providers: [
            {
              provide: HttpAuthService,
              useValue: {
                logout: () => { }
              }
            }
          ]
        });
        component = fixture.componentInstance;
        fixture.detectChanges();
      });
    
      it('should create', () => {
        expect(component).toBeTruthy();
      });
    });