DEV Community

loading...

Testing an Effect using observer-spy

alfredoperez profile image Alfredo Perez ・Updated on ・2 min read

Updates

  • September 29 2020: refactor to use fakeTime and subscribeAndSpyOn as recommended by Shai Reznik πŸŽ‰

  • October 07 2020: reafactor to use subscribeSpyTo as recommended by Shai Reznik πŸŽ‰


Have you try theobserver-spy library by Shai Reznik?

It particularly makes testing ngrx effects an easy task and keeps them readable.

To demonstrate this, I refactored the tests from book.effects.spec.ts from the ngrx example application, and here are the differences...

Testing the success path

Using marbles:

 it('should return a book.SearchComplete, with the books, on success, after the de-bounce', () => {
    const book1 = { id: '111', volumeInfo: {} } as Book;
    const book2 = { id: '222', volumeInfo: {} } as Book;
    const books = [book1, book2];
    const action = FindBookPageActions.searchBooks({ query: 'query' });
    const completion = BooksApiActions.searchSuccess({ books });

    actions$ = hot('-a---', { a: action });
    const response = cold('-a|', { a: books });
    const expected = cold('-----b', { b: completion });
    googleBooksService.searchBooks = jest.fn(() => response);

    expect(
      effects.search$({
        debounce: 30,
        scheduler: getTestScheduler(),
      })
    ).toBeObservable(expected);
  });

Using observer-spy:

 it('should return a book.SearchComplete, with the books, on success, after the de-bounce', fakeTime((flush) => {
        const book1 = { id: '111', volumeInfo: {} } as Book;
        const book2 = { id: '222', volumeInfo: {} } as Book;
        const books = [book1, book2];

        actions$ = of(FindBookPageActions.searchBooks({ query: 'query' }));
        googleBooksService.searchBooks = jest.fn(() => of(books));

        const effectSpy = subscribeSpyTo(effects.search$());

        flush();
        expect(effectSpy.getLastValue()).toEqual(
          BooksApiActions.searchSuccess({ books })
        );
      })
    );

Testing the error path

Using marbles:

  it('should return a book.SearchError if the books service throws', () => {
      const action = FindBookPageActions.searchBooks({ query: 'query' });
      const completion = BooksApiActions.searchFailure({
        errorMsg: 'Unexpected Error. Try again later.',
      });
      const error = { message: 'Unexpected Error. Try again later.' };

      actions$ = hot('-a---', { a: action });
      const response = cold('-#|', {}, error);
      const expected = cold('-----b', { b: completion });
      googleBooksService.searchBooks = jest.fn(() => response);

      expect(
        effects.search$({
          debounce: 30,
          scheduler: getTestScheduler(),
        })
      ).toBeObservable(expected);
    });

Using observer-spy:

 it('should return a book.SearchError if the books service throws', fakeTime((flush) => {
        const error = { message: 'Unexpected Error. Try again later.' };
        actions$ = of(FindBookPageActions.searchBooks({ query: 'query' }));
        googleBooksService.searchBooks = jest.fn(() => throwError(error));

        const effectSpy = subscribeSpyTo(effects.search$());

        flush();
        expect(effectSpy.getLastValue()).toEqual(
          BooksApiActions.searchFailure({
            errorMsg: error.message,
          })
        );
      })
    );

Testing when the effect does not do anything

Using marbles:

   it(`should not do anything if the query is an empty string`, () => {
      const action = FindBookPageActions.searchBooks({ query: '' });

      actions$ = hot('-a---', { a: action });
      const expected = cold('---');

      expect(
        effects.search$({
          debounce: 30,
          scheduler: getTestScheduler(),
        })
      ).toBeObservable(expected);
    });

Using observer-spy:

 it(`should not do anything if the query is an empty string`, fakeTime((flush) => {
        actions$ = of(FindBookPageActions.searchBooks({ query: '' }));

        const effectSpy = subscribeSpyTo(effects.search$());

        flush();
        expect(effectSpy.getLastValue()).toBeUndefined();
      })

You can find the working test here:

https://github.com/alfredoperez/ngrx-observer-spy/blob/master/projects/example-app/src/app/books/effects/book.effects.spec.ts

What do you think? Which one do you prefer?

Discussion (8)

pic
Editor guide
Collapse
shairez profile image
Shai Reznik

Great job Alfredo!

Few things I would try that might make your tests even shorter -

  1. You can use fakeTime and flush() to remove the need of using the TestScheduler.

  2. You can use the factory function subscribeSpyTo to subscribe and create the spy at the same time (and you can also auto-unsubscribe as well)

Let me know if it helps

Collapse
alfredoperez profile image
Alfredo Perez Author

Thank you, Shai!

I think it looks better, readable and fewer lines of code. Updated the article and repo.

I was not able to find subscribeSpyTo and used subscribeAndSpyOn instead.

Collapse
shairez profile image
Shai Reznik

Awesome Alfredo!

Try to update to the latest version (1.4.0) and you'll see subscribeSpyTo

Thread Thread
alfredoperez profile image
Alfredo Perez Author

Wooot!

I will update the repo and the article. Thanks!

Thread Thread
shairez profile image
Shai Reznik • Edited

Nice!

Pay attention that you've missed 1 subscribeAndSpyOn ... πŸ˜€

Thread Thread
alfredoperez profile image
Alfredo Perez Author

Thanks! it should be good now =)

Thread Thread
shairez profile image
Shai Reznik

Pair programming FTW! πŸ’ͺπŸ˜€

Collapse
scooperdev profile image
Stephen Cooper

Been loving this lib! Had missed the recent updates which make it even cleaner to use.