DEV Community

Alfredo Perez
Alfredo Perez

Posted on • Edited on

Testing an Effect using observer-spy

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);
  });
Enter fullscreen mode Exit fullscreen mode

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 })
        );
      })
    );
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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,
          })
        );
      })
    );
Enter fullscreen mode Exit fullscreen mode

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);
    });
Enter fullscreen mode Exit fullscreen mode

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();
      })
Enter fullscreen mode Exit fullscreen mode

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?

Top comments (8)

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

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

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

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.