Writing an example effect to test
Imagine you have written an effect to load tasks but only when they are not loaded yet.
const loadTasks = createEffect( () => { const actions$ = inject(Actions); const toDoApi = inject(TaskApi); const store = inject(Store);
return actions$.pipe( ofType(ToDoActions.loadTasks), switchMap(() => store.select(selectTaskState).pipe(take(1))), switchMap((state) => state.status === "LOADED" ? EMPTY : toDoApi.getTasks().pipe( map((toDos) => ToDoActions.tasksLoaded({ tasks: toDos })), catchError(() => EMPTY), ), ), ); }, { functional: true },);
export const TasksEffects = { loadTasks };
Testing scenarios
Let’s now write testing scenarios. There should be three tests:
- should call
getTasks
method when the state is notLOADED
- should not call
getTasks
method when the state isLOADED
- should call
getTasks
only once even whenToDoActions.loadTasks
is called twice
Writing a helper function for test cases
Each scenario will have the same configuration logic so I have created a function to not repeat it each time.
function createContext() { const actions$ = new Subject(); const mockTaskApi = jasmine.createSpyObj<TaskApi>(TaskApi.name, ["getTasks"]);
mockTaskApi.getTasks.and.returnValue(timer(100).pipe(map(() => [])));
const injector = Injector.create({ providers: [ provideMockStore({ initialState: { task: initialState } }), provideMockActions(() => actions$), { provide: TaskApi, useValue: mockTaskApi }, ], });
return { injector, actions$, mockToDoApi: mockTaskApi, mockStore: injector.get<MockStore<{ task: TaskState }>>(MockStore), };}
You can do the same with beforeEach
function available in Jasmine.
Writing the test to check if getTasks
has been called
To check if the method getTasks
is called we have to do three things
- listen for the result of the effect by using
firstValueFrom
- emit action to load tasks
- check what effect has returned
it('should call "getTasks" action when the status is INITIAL', async () => { const { actions$, injector, mockToDoApi, mockStore } = createContext();
mockStore.setState({ task: { status: "INITIAL" } });
await runInInjectionContext(injector, async () => { const effect = TasksEffects.loadTasks();
const resultPromise = firstValueFrom(effect); actions$.next(ToDoActions.loadTasks);
expect(await resultPromise).toEqual(ToDoActions.tasksLoaded({ tasks: [] })); expect(mockToDoApi.getTasks).toHaveBeenCalledTimes(1); });});
Writing the test to check if getTasks
has not been called
The test will be similar to the previous one, but the difference is the initial state. Here the initial state will be with the status LOADED
.
it('should not call "loadItems" action when the status is LOADED', async () => { const { actions$, injector, mockToDoApi, mockStore } = createContext();
mockStore.setState({ task: { status: "LOADED", tasks: [] } });
await runInInjectionContext(injector, async () => { const effect = TasksEffects.loadTasks();
const resultPromise = firstValueFrom(effect, { defaultValue: "END" }); actions$.next(ToDoActions.loadTasks); actions$.complete();
expect(await resultPromise).toEqual("END"); expect(mockToDoApi.getTasks).toHaveBeenCalledTimes(0); });});
defaultValue
will be returned when the subscription (the effect in this case) is completed without emitting any value. That’s so that we can await
our promise. Without it, the promise will be rejected.
Writing the test to check if getTasks
will not be called twice
In this test, we want to check the case when a user calls the action twice.
We want to handle that case by ignoring the next action.
The test should check if the method getTasks
has been called only once even when the action was called twice.
it('should call "getTasks" only once ', async () => { const { actions$, injector, mockToDoApi, mockStore } = createContext();
mockStore.setState({ task: { status: "INITIAL" } });
await runInInjectionContext(injector, async () => { const effect = TasksEffects.loadTasks();
const resultPromise = firstValueFrom(effect); actions$.next(ToDoActions.loadTasks); actions$.next(ToDoActions.loadTasks);
expect(await resultPromise).toEqual(ToDoActions.tasksLoaded({ tasks: [] })); expect(mockToDoApi.getTasks).toHaveBeenCalledTimes(1); });});
This test will fail because the effect does not have protection for this case.
To handle this case we should replace switchMap
with exhaustMap
in effect implementation.
// switchMap((state) => state.status === 'LOADED' /** ... */)exhaustMap((state) => state.status === "LOADED" /** ... */);