Test Effects
First, let's quickly review the search effect in src/app/state/resort/resort.effects.ts:
export class ResortEffects {
@Effect()
search: Observable<Action> = this.actions.pipe(
ofType < SearchResorts > ResortActionTypes.SearchResorts,
exhaustMap(action => {
return this.resortService.search(action.q).pipe(
map(resorts => new SearchResortsSuccess(resorts)),
catchError(error => of(new SearchResortsFail(error)))
);
})
);
}- The
searcheffect returns anObservableof anAction. - When the
SearchResortsaction is dispatched we invoke thesearch()method on theResortService. - In the event that the HTTP request returns a 200 OK status we use the
map()operator to return theSearchResultsSuccessaction. TheexhaustMap()operator returns the inner obsevable of the new action. - In the event that the HTTP request is not a 200 OK we catch the exception using the
catchError()operator and return a new observableof()theSearchResortsFailaction, specifying theerrorobject.
Setup
Open the src/app/state/resort/resort.effects.spec.ts file.
Before we can test an effect we need to do a little setup for our tests using the beforeEach() method:
describe('ResortEffects', () => {
let actions: Observable<any>;
let effects: ResortEffects;
let resortService: ResortService;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
ResortEffects,
{
provide: ResortService,
useValue: {}
},
provideMockActions(() => actions)
]
});
actions = TestBed.get(Actions);
effects = TestBed.get(ResortEffects);
resortService = TestBed.get(ResortService);
});
});- Define
actionsblock scope variable within thedescribecallback function that is anObservable. This observable stream will represent the actions dispatched by the store. - Define
resortService. - Provide the
ResortEffectsservice dependency. - Provide the
ResortServicedependency and mock it out as an empty object. - Use the
provideMockActionsfunction and specify a factory function that returns theactionsobservable stream. - Use the
TestBed.get()method to get the provided objects.
closeDialogsOnSelect
Test the closeDialogsOnSelect effect:
describe('closeDialogOnSelect', () => {
it('should dispatch the CloseDialogs action', () => {
const resort = generateResort();
const action = new SelectResort(resort);
const outcome = new CloseDialogs();
actions = hot('-a', { a: action });
const expected = cold('-a', { a: outcome });
expect(effects.closeDialogOnSelect).toBeObservable(expected);
});
});- Generate a fake
Resortobject using thegenerateResort()function. - Declare the
actionto that will be mocked:SelectResort, specifying theresortthat is being selected. - Declare the
outcomeaction that will be emitted by thecloseDialogOnSelecteffect. - Mock the
hot()actions observable. After 10 frames the observable emits a next notification (represented by the 'a' character) whose value is theaction. - The
expectedobservable is acold()observable that emits theoutcomevalue after 10 frames. - Expect the
closeDialogOnSelecteffect to be theexpectedobservable.
search Happy Path
Test the happy (or success) path:
describe('search', () => {
it('should dispatch the SearchResortsSuccess action on success', () => {
const q = 'Testing';
const resorts = [generateResort()];
const action = new SearchResorts(q);
const outcome = new SearchResortsSuccess(resorts);
actions = hot('-a', { a: action });
const response = cold('-a|', { a: resorts });
const expected = cold('--b', { b: outcome });
resortService.search = jest.fn(() => response);
expect(effects.search).toBeObservable(expected);
});
});- First we define a fake user query in the
qconstant. - Using the
generateResort()function we declare an array ofResortobjects. - Declare the
actionthat we are testing, in this case theSearchResortsaction, specifying the necessary constructor arguments. - Declare the
outcomenext notification action that thesearchobservable should emit. In this case the outcome should be theSearchResortsSuccessaction. - Mock the
actionsobservable stream with a testhot()observable. The marble syntax mocks an observable that emits a next notification (represented by the 'a' character) after 10 frames (single dash). The value of the next notification (a) is theactionwe are testing. - Declare the
responseas acold()observable. The marble syntax instructs the observable to mock 10 frames (a single dash) followed by a next notification (represented by the 'a' character) followed by a completion notification (the pipe character). The value of the next notification (a) is theresortsarray. - Declare the
expectedobservable stream with a testcold()observable. The marble syntac mocks an observable that is 30 frames in duration. 20 frames has passed before the next notification because theactionobservable specified 10 frames (one dash) before emitting the next notification, and theresponseobservable also specified 10 frames before emitting the next notification. 10 + 10 = 20. Therefore, we expect that after 20 frames theoutcomeaction will be emitted by thesearcheffect. - Expect the
searcheffect to be theexpectedobservable using thetoBeObservable()method.
search Failure Path
We also need to test the search effect's failure path:
describe('search', () => {
it('should dispatch the SearchResortsFail action on failure', () => {
const q = 'Testing';
const error = new Error('Test Error');
const action = new SearchResorts(q);
const outcome = new SearchResortsFail(error);
actions = hot('-a', { a: action });
const response = cold('-#', {}, error);
const expected = cold('--b', { b: outcome });
resortService.search = jest.fn(() => response);
expect(effects.search).toBeObservable(expected);
});
});- The failure path test is very similar to the happy path test.
- Declare an
errorblock scoped constant that is anErrorobject. - The
outcomeaction is theSearchResortsFailaction whose payload is anerror. - The
responseobservable emits an error notification (the hash symbol or pound sign) after 10 frames.
Execute Tests
Execute a test run via:
npm test
yarn testYou can also start the watch mode via:
npm run test:watch
yarn test:watch