Have you tried the observer-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 makes it easier to arrange what we want to test, as we don't have to create a representation of the observables using characters, as demonstrated in the code below:
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 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 is more readable because we are "acting" before the expectation, as opposed to when using marbles, where the "acting" part of the test is done inside the expect statement.
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();
})
Testing the error path of an effect
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 is clear what action should be triggered and expected.
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,
})
);
})
);
You can find the working example here: