0

I want to test this observable but I get NOTHING back from it:

const fetchUsersApi = (action$: any) => action$.pipe(
    ofType(FETCH_USERS),
    mergeMap(() => from(
        API.graphql(graphqlOperation(queries.LIST_ALL_USERS)),
    ).pipe(
        map((res: any) => fetchUsersSuccess(
            // TODO - standardise this to 'items' so fetchApi can be used
            pathOr(null, ['data', 'listAllUsers', 'users'], res),
        )),
        catchError((error: Error) => {
            const processedError = processAppSyncErrorMessage(error);
            console.log('Error', processedError);
            return of(addError(processedError), fetchUsersError(processedError));
        }),
    )),
);

The test is as follows:

import { TestScheduler } from 'rxjs/testing';

import { API } from 'aws-amplify';
import { ActionsObservable } from 'redux-observable';
import { fetchUsersSuccess, fetchUsers } from '../actions';

import fetchUsersEpic from './fetchUsersApi';

jest.mock('aws-amplify');

const testScheduler = new TestScheduler(
    (actual, expected) => expect(actual).toStrictEqual(expected),
);

describe('fetchUsersEpic', () => {
    it('should fetch the users', () => {
        const mockFetchedUsers = [
            { id: 'fakeUser1' },
        ];
        (API.graphql as jest.Mock).mockImplementation(() => Promise.resolve({
            data: { listAllUsers: { users: mockFetchedUsers } },
        }));
        testScheduler.run((helpers) => {
            const {
                hot,
                cold,
                expectObservable,
                flush,
            } = helpers;
            const action$ = cold('-a', {
                a: fetchUsers(),
            });

            const reduxObservableaAction = ActionsObservable.from(action$);

            const actual = fetchUsersEpic(reduxObservableaAction);
            const expectedMarbles = '-b';
            const expectedValues = { b: fetchUsersSuccess(mockFetchedUsers) };

            expectObservable(actual).toBe(expectedMarbles, expectedValues);
            flush();
        });
    });
});

The result I get back is:

● fetchUsersEpic › should fetch the users

expect(received).toStrictEqual(expected) // deep equality

  • Expected - 18
  • Received + 1

Obviously I'm missing something here, I was thinking the value returned should be the fetchUsersSuccess() callback with some data but instead we get an empty array. Would be great to get some ideas.

pip
  • 453
  • 2
  • 13
  • Try mocking `API.graphql` to return an observable rather than a promise. Promise callbacks are executed asynchronously – NickL Oct 26 '21 at 11:35
  • I just decided to black box at the store level and dispatch events and wrote a middleware that has a jest.fn() passed in to receive any actions on the store. Worked fine even though it's hardly a unit test! – pip Oct 27 '21 at 08:56
  • I'm glad you got something working. Bear in mind in future that any time you introduce Promises you'll need to wait for the result, not just with marble tests! – NickL Oct 27 '21 at 09:16

1 Answers1

0

I decided to ignore the testing solutions provided and do a test of the whole store. It works better and if we decide to bin redux-observable we can still keep the tests.

import {
    applyMiddleware, createStore, Action,
} from 'redux';

import { composeWithDevTools } from 'redux-devtools-extension';
import { createEpicMiddleware, combineEpics, Epic } from 'redux-observable';

import rootReducer from '../../store/reducers';

/**
 * makeActionListMiddleware is a function to allow us to pass a jest mock
 * function to the store to pick up on any actions that are called on it
 */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const makeActionMiddleware = (jestMockFunction: jest.Mock) => (store: object) => (next: any) => (action: Action<any>) => {
    jestMockFunction(action);
    // continue to the next action
    return next(action);
};

// creates a fake version of the store with selected epics combined and a jest
// function middleware that received all store events.
const createEpicTester = (
    epics: Epic[],
    reducers = rootReducer,
    initialState = {},
) => {
    const storeActionJestCallback = jest.fn();
    const jestActionMiddleware = makeActionMiddleware(storeActionJestCallback);
    const composeEnhancers = composeWithDevTools({});
    const epicMiddleware = createEpicMiddleware();
    const store = createStore(reducers, initialState, composeEnhancers(
        applyMiddleware(epicMiddleware, jestActionMiddleware),
    ));
    epicMiddleware.run(combineEpics(...epics));
    return { store, dispatch: store.dispatch, storeActionJestCallback };
};

export default createEpicTester;

Usage

const { dispatch, storeActionJestCallback } = createEpicTester([epic]);
dispatch({ type: "someEvent" });
expect(storeActionJestCallback).toHaveBeenCalledWith({ type: "someEvent" }); 

You can now expect further events that your epic applied to the store.

pip
  • 453
  • 2
  • 13