Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit testing NgRx effect to ensure the service method was called - ain't working

I am using NgRx ^7.0.0 version. This is my NgRx effect class:

import { Injectable } from '@angular/core';
import { ApisService } from '../apis.service';
import { Effect, Actions, ofType } from '@ngrx/effects';
import { Observable } from 'rxjs';
import { ApisActionTypes, ApisFetched } from './apis.actions';
import { mergeMap, map } from 'rxjs/operators';

@Injectable()
export class ApisEffects {

  constructor(private apisS: ApisService, private actions$: Actions) { }

  @Effect()
  $fetchApisPaths: Observable<any> = this.actions$.pipe(
    ofType(ApisActionTypes.FetchApisPaths),
    mergeMap(() =>
      this.apisS.fetchHardCodedAPIPaths().pipe(
        map(res => new ApisFetched(res))
      )
    )
  );
}

And that's a simple test. As you can see it should fail, but is always passing. I followed similar question here on StackOverflow How to unit test this effect (with {dispatch: false})? but it doesn't work for me, as if the code execution never enters the effects.$fetchApisPaths.subscribe block

import { TestBed } from '@angular/core/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { hot, cold } from 'jasmine-marbles';
import { Observable, ReplaySubject } from 'rxjs';
import { ApisEffects } from '../state/apis.effects';
import { ApisFetch, ApisFetched } from '../state/apis.actions';
import { IApiPath } from '../models';
import { convertPaths, getAPIPathsAsJson, ApisService } from '../apis.service';
import { ApisServiceMock } from './mocks';

describe('Apis Effects', () => {
  let effects: ApisEffects;
  let actions: Observable<any>;
  let apisS: ApisService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        ApisEffects,
        provideMockActions(() => actions),
        {
          provide: ApisService,
          useClass: ApisServiceMock
        }
      ]
    });

    effects = TestBed.get(ApisEffects);
    apisS = TestBed.get(ApisService);
  });

  it('should call ApisService method() to get Api Paths', () => {
    const spy = spyOn(apisS, 'fetchHardCodedAPIPaths');

    const action = new ApisFetch();
    actions = hot('--a-', {a: action});

    effects.$fetchApisPaths.subscribe(() => {
      console.log('%c effect trigerred', 'color: orange; border: 1px solid red;');
      // expect(spy).toHaveBeenCalled();
      expect(true).toBe(false); // never fails
    });
  });
});

Just in case I am doing smthg stupid with actions, here is the actions file: Most likely I am not, since it's working in the app as expected.

import { Action } from '@ngrx/store';
import { IApiPath } from '../models';

export enum ApisActionTypes {
    FetchApisPaths = '[Apis] Fetch Paths',
    FetchedApisPaths = '[Apis] Fetched Paths'
}

export class ApisFetch implements Action {
    readonly type = ApisActionTypes.FetchApisPaths;
}

export class ApisFetched implements Action {
    readonly type = ApisActionTypes.FetchedApisPaths;
    constructor(public payload: IApiPath[]) {}
}

export type ApisActions = ApisFetch | ApisFetched;

=======================EDIT==============================

I have used an example from official ngrx docs https://ngrx.io/guide/effects/testing and now I can successfully enter the subscribe block below, I get both console logs logged, but the test succeeds. This is bizarre! I have tried throwing errors from the subscribe block and the test still succeeds.

it('should work also', () => {
    actions$ = new ReplaySubject(1);

    actions$.next(new ApisFetch());

    effects.$fetchApisPaths.subscribe(result => {
      console.log('will be logged');
      expect(true).toBe(false); // should fail but nothing happens - test succeeds
      console.log(' --------- after '); // doesn't get called, so the code
      // execution stops on expect above
    });
  });
like image 557
codeepic Avatar asked Jan 21 '19 15:01

codeepic


1 Answers

Ok, so I got it working. In order to successfully test whether a specific Angular service method is called from within NgRx effect, I wrapped a test case in an async:

  it('should call ApisService method to fetch Api paths', async () => {
    const spy = spyOn(apisS, 'fetchHardCodedAPIPaths');

    actions$ = new ReplaySubject(1);
    actions$.next(new ApisFetch());
    await effects.$fetchApisPaths.subscribe();
    
    expect(spy).toHaveBeenCalled();
  });

I await effects.$fetchApisPaths.subscribe(); to block the execution and run test assertion in the next line.

Now when I try to run expect(true).toBe(false); to test whether the test fails, it properly fails.

The problem with my code in the question (the example with ReplaySubject as in ngrx docs https://ngrx.io/guide/effects/testing ) was that it was not possible to fail a test when the assertion was inside .subscribe() block. Something iffy was going on in there and I still don't know exactly why the code was behaving in the following manner:

effects.$fetchApisPaths.subscribe(result => {
  console.log('will be logged');  // 1) gets logged
  expect(true).toBe(false);       // 2) should fail
  console.log(' - after ');       // 3) doesn't get called
});  

So the code execution stops on line 2), test case returns positive and line 3) never gets executed.

So the test case in ngrx docs with assertion inside .subscribe() block will always be green, giving you a false positive for your test case. This is the behaviour I experienced with ngrx ^7.0.0

EDIT September 2020 - updated for ngrx version 9. If the solution above doesn't work for you, or for me in the future, because yet again I face the same problem and find only my own answer to help and great comment from @Christian to lead me to ngrx gitter question, try this approach:

 it('should call ApisService method to fetch Api paths', async () => {
  const spy = spyOn(apisS, 'fetchHardCodedAPIPaths');

  actions$ = cold('--a-', {
      a: ControlCenterTrendsLineChartPeriodChange({ numberOfMonths: 24 })
  });
  await effects.$fetchApisPaths.subscribe();

  expect(actions$).toSatisfyOnFlush(() => {
      expect(spy).toHaveBeenCalled();
});
like image 94
codeepic Avatar answered Sep 26 '22 13:09

codeepic