How to test an NgRx Effect by mocking an Http Response

Viewed 1603

Objective: I'd like to write a proper unit test for a basic NgRx Effect. So far, none of my attempts haven't been successful when using jasmine-marbles.

Here is the effect i'm trying to test:

markMovieAsFinished$ = createEffect(() =>
  this.actions$.pipe(
    ofType(MovieActions.markMovieAsFinished),
    switchMap((action: Action) => {
      return this.http.put(`/api/reading-list/${action.item.movieId}/finished`, { finishedDate: action.item.finishedDate })
      .pipe(
          switchMap(() => EMPTY) // With an optimistic update, we initially handle the 'markMovieAsFinished' action directly in the reducer, hence, no need to dispatch another action on a successful PUT request!
      );
    })
  )
);

And here is my latest attempt using marble testing. (i'm very new at implementing marble testing!):

describe('MovieEffects', () => {
  let actions$: Actions;
  let effects: MovieEffects;
  let httpMock: HttpTestingController;


  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ NxModule.forRoot(), SharedTestingModule ],
      providers: [
        MovieEffects,
        DataPersistence,
        provideMockActions(() => actions$)
      ]
    });

    actions$ = TestBed.inject(Actions);
    effects = TestBed.inject(MovieEffects);
    httpMock = TestBed.inject(HttpTestingController);
  });


  describe('markMovieAsFinished$', () => {
    it('should return an EMPTY observable with the outcome, on success', () => {
    const action: Action = MovieActions.markMovieAsFinished({ finishedDate: '1/1/20'});
    const completion = EMPTY;

    // setup the Effect
    actions$ = hot('-a', { a: action });
    const response = cold('-b|', { b: {} }); // payload from successful response can just be an empty object
    const expected = cold('--c', { c: completion });

    expect(effects.markMovieAsFinished$).to.be.eql(expected);
    
    })
  });

When I try the above attempt, the markMovieAsFinished$ value returns an empty array. In the above code, I've setup the arrange and assert parts of the unit test.The part I'm not understanding is how to mock a successful PUT Request to test this effect.

3 Answers

I would move that HTTP call to a service and then inject that service into the effect. This would make testing the service and the effect easier and has better separation of concerns. When testing the effect, you would mock this service.

Nonetheless, we will create a fake HTTP for your requirement:

1.) Get rid of HttpTestingController (you don't need it in this case)

2.) Create a fake HTTP with put method.

3.) Return the mock value for the fake HTTP and then do assertion

describe('MovieEffects', () => {
  let actions$: Actions;
  let effects: MovieEffects;
  let mockHttpClient = any;

  beforeEach(() => {
    TestBed.configureTestingModule({
      // the name ('http') goes as the first argument and an array of public methods you want to spyOn
      mockHttpClient = jasmine.createSpyObj('http', ['put']);
      imports: [ NxModule.forRoot(), SharedTestingModule ],
      providers: [
        MovieEffects,
        DataPersistence,
        provideMockActions(() => actions$),
        { provide: HttpClient, useValue: mockHttpClient }
      ]
    });

    actions$ = TestBed.inject(Actions);
    effects = TestBed.inject(MovieEffects);
  });


  describe('markMovieAsFinished$', () => {
    it('should return an EMPTY observable with the outcome, on success', () => {
    const action: Action = MovieActions.markMovieAsFinished({ finishedDate: '1/1/20'});
    const completion = EMPTY;

    // setup the Effect
    actions$ = hot('-a', { a: action });
    const response = cold('-b|', { b: {} }); // payload from successful response can just be an empty object
    const expected = cold('--c', { c: completion });
    mockHttpClient.put.and.returnValue(response); // make the mockHttpClient return fake response
    // do your assertion
    expect(effects.markMovieAsFinished$).to.be.eql(expected);
    
    })
  });

I have no clue what a marble testing is, but I do know that i'm building my effects differently.

markMovieAsFinished$ = this.actions$.pipe(
    ofType(MovieActions.markMovieAsFinished),
    mergeMap((action) => {
        return this.http.put(`/api/reading-list/${action.item.movieId}/finished`, { finishedDate: action.item.finishedDate }).pipe(
        map((resp) => Success,
        catchError(() => Failure)
        );
    })
    );

Hope it can help

To test the put part of the Effect you will only need to test that the right parameters were called

spyOnProperty(action,'item').and.returnValue({movieId : 1});

spyOn(effects.http, 'put').and.callFake((url: string, data: any) => {
  expect(url).toEqual('/api/reading-list/1/finished');
  expect(data).toEqual({ finishedDate: '1/1/20' });
})
Related