angularunit-testingngrxngrx-storengrx-effects

NgRx testing - NullInjectorError: No provider for Service


I'm trying to write unit tests for effects and I got the error NullInjectorError: No provider for StationsListService!.

My stations.effects.ts is:

@Injectable()
export class StationsListEffects {
  constructor(private actions$: Actions, private stationsListService: StationsListService) {}

  @Effect()
  loadStationsList$ = this.actions$.pipe(
    ofType(StationsListActionTypes.LoadStationsList),
    exhaustMap(() => this.stationsListService.getStations()),
    map((stationsList: StationListItem[]) => new StationsLoaded(stationsList)),
    catchError((error: Error) => of(new StationsLoadFailed(error)))
  );
}

And the stations.effects.spec.ts is:

describe('StationsListEffects', () => {
  let actions: Observable<any>;

  let effects: StationsListEffects;
  let stationsListService: jasmine.SpyObj<StationsListService>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        StationsListService,
        StationsListEffects,
        provideMockActions(() => actions),
        {
          provide: StationsListService,
          useValue: {
            getStations: jasmine.createSpy(),
          },
        },
      ],
    });

    effects = TestBed.inject(StationsListEffects);
    stationsListService = TestBed.inject(StationsListService);
  });

  describe('loadStationsList', () => {
    it('should return a stream with stations list loaded action', () => {
      const stationsList: StationListItem[] = [
        {
          id: '123',
          identifier: 'identifier 123',
          name: 'west',
          address: {
            city: 'sv',
            streetAndHouseNumber: 'str. Universitatii 13',
            postcode: '720234',
            state: 'sv',
          },
          active: false,
        },
      ];
      const action = new LoadStationsList();
      const outcome = new StationsLoaded(stationsList);

      actions = hot('-a', { a: action });
      const response = cold('-a|', { a: stationsList });
      stationsListService.getStations.and.returnValue(response);

      const expected = cold('--b', { b: outcome });
      expect(effects.loadStationsList$).toBeObservable(expected);
    });

    it('should fail and return an action with the error', () => {
      const action = new LoadStationsList();
      const error = new Error('some error') as any;
      const outcome = new StationsLoadFailed(error);

      actions = hot('-a', { a: action });
      const response = cold('-#|', {}, error);
      stationsListService.getStations.and.returnValue(response);

      const expected = cold('--(b|)', { b: outcome });
      expect(effects.loadStationsList$).toBeObservable(expected);
    });
  });
});

At the stationsListService = TestBed.inject(StationsListService); I have an error that says: Type 'StationsListService' is not assignable to type 'SpyObj<StationsListService>'.

The stations-list.service.ts is:

@Injectable()
export class StationsListService {
  private stationList: StationListItem[] = [];

  public get stationsList(): StationListItem[] {
    return this.stationList;
  }

  private baseUrl = '//localhost:8080/api/stations';

  constructor(private httpClient: HttpClient) {}

  public getStations(): any {
    return this.httpClient.get<Array<StationListItem>>(this.baseUrl).pipe(
      tap((data) => {
        this.stationList = data;
      })
    );
  }

  public addStation(station: StationListItem): any {
    return of(null).pipe(delay(2000));
  }

  public updateStation(station: StationListItem): any {
    return of(null).pipe(delay(2000));
  }

  public deleteStation(id: string): any {
    return of(null).pipe(delay(2000));
  }
}

I tried to inject service as a SpyObj like stationsListService = TestBed.inject(StationsListService) as jasmine.SpyObj<StationsListService>;, but still doesn't work. Does anybody have any ide how to solve this?


Solution

  • The compile error is quite clear as you are trying to assign a type (return by TestBed.inject) to a type of spy which is not compatible. To fix the error, first, change the type of the service and then use spyOn to spy the method on the service. let's update the code like this -

    describe('StationsListEffects', () => {
      let actions: Observable<any>;
    
      let effects: StationsListEffects;
      let stationsListService: StationsListService;
    
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            StationsListService,
            StationsListEffects,
            provideMockActions(() => actions)
          ],
        });
    
        effects = TestBed.inject(StationsListEffects);
        stationsListService = TestBed.inject(StationsListService);
      });
    
      describe('loadStationsList', () => {
        it('should return a stream with stations list loaded action', () => {
          const stationsList: StationListItem[] = [
            {
              id: '123',
              identifier: 'identifier 123',
              name: 'west',
              address: {
                city: 'sv',
                streetAndHouseNumber: 'str. Universitatii 13',
                postcode: '720234',
                state: 'sv',
              },
              active: false,
            },
          ];
          //SPY the function and return mocked data wrapped in an observable using "of" operator
          spyOn(stationsListService, 'getStations').and.returnValue(of(stationsList));
          const action = new LoadStationsList();
          const outcome = new StationsLoaded(stationsList);
    
          actions = cold('-a', { a: action });
          const expected = cold('--b', { b: outcome });
          expect(effects.loadStationsList$).toBeObservable(expected);
        });
      });
    });