Search code examples
javascriptreactjsunit-testingjestjsredux-saga

Need help in React Saga Unit test


I am facing an isse while testing my saga function:

function * onSaveDATA() {
  try {
    yield put( showStatusMessage({ messageContent: 'Saving Your Data' }));
    const body = yield select( state => state.appData.userDetails ); 
    yield call( postDATA, { body });
    yield put( hideStatusMessage());
    yield put({ type: ActionTypes.SAVE_DATA_OK });
  } catch ( e ) {
    yield put({ type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: e });
  }
}

export function * save_on_change( ) {
  yield takeEvery( ActionTypes.SAVE_DATA_REQ, onSaveDATA );
}

Here is a unit test which I have written to test this function, but it is failing the test. I am not sure what is wrong.

import { runSaga } from 'redux-saga';

import { postDATA } from './../../../../services/my_service';

// import { openModalMessage } from './../../../../actions';

import { saveDATA } from './';

jest.mock( './../../../../services/my_service' );
jest.mock( './../../../../actions' );


describe( 'Saga: Save Data', () => {
  test( 'saveDATA OK', async () => {

    postDATA.mockReset();
    postDATA.mockReturnValue( {s:'Somevalue'} );
    const dispatchedActions = [];
    await runSaga({
      dispatch: action => dispatchedActions.push( action ),
      getState: () => ({
        appState: {},
        appData: { userDetails: {name:'mock-name'}},
      }),
    }, save_on_change );
    expect( postDATA ).toHaveBeenCalled();
      });
});

When I run this it fails. I am not sure what am I missing here. Is it because the saveDATA function is using factory function takeEvery. Do I need to explicilty trigger the action SAVE_DATA_REQ?


Solution

  • Here is unit test solution for "redux-saga": "^1.1.3":

    index.ts:

    import { put, select, call, takeEvery } from 'redux-saga/effects';
    import { postDATA } from './service';
    
    export const ActionTypes = {
      SAVE_DATA_OK: 'SAVE_DATA_OK',
      CRITICAL_ERROR_OCCURED: 'CRITICAL_ERROR_OCCURED',
      SAVE_DATA_REQ: 'SAVE_DATA_REQ',
    };
    
    const showStatusMessage = (payload) => ({ type: 'SHOW_STATUS_MESSAGE', payload });
    const hideStatusMessage = () => ({ type: 'HIDE_STATUS_MESSAGE' });
    
    export function* onSaveDATA() {
      try {
        yield put(showStatusMessage({ messageContent: 'Saving Your Data' }));
        const body = yield select((state) => state.appData.userDetails);
        yield call(postDATA, { body });
        yield put(hideStatusMessage());
        yield put({ type: ActionTypes.SAVE_DATA_OK });
      } catch (e) {
        yield put({ type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: e });
      }
    }
    
    export function* save_on_change() {
      yield takeEvery(ActionTypes.SAVE_DATA_REQ, onSaveDATA);
    }
    

    service.ts:

    export async function postDATA(data) {
      return { s: 'real data' };
    }
    

    index.test.ts:

    import { runSaga } from 'redux-saga';
    import { onSaveDATA, ActionTypes, save_on_change } from './';
    import { postDATA } from './service';
    import { mocked } from 'ts-jest/utils';
    import { takeEvery } from 'redux-saga/effects';
    
    jest.mock('./service');
    
    describe('62952662', () => {
      afterAll(() => {
        jest.resetAllMocks();
      });
      describe('onSaveDATA', () => {
        test('should save data', async () => {
          mocked(postDATA).mockResolvedValueOnce({ s: 'Somevalue' });
          const dispatchedActions: any[] = [];
          await runSaga(
            {
              dispatch: (action) => dispatchedActions.push(action),
              getState: () => ({
                appState: {},
                appData: { userDetails: { name: 'mock-name' } },
              }),
            },
            onSaveDATA,
          ).toPromise();
          expect(postDATA).toBeCalledWith({ body: { name: 'mock-name' } });
          expect(dispatchedActions).toEqual([
            { type: 'SHOW_STATUS_MESSAGE', payload: { messageContent: 'Saving Your Data' } },
            { type: 'HIDE_STATUS_MESSAGE' },
            { type: ActionTypes.SAVE_DATA_OK },
          ]);
        });
    
        test('should handle error if postDATA error', async () => {
          const mError = new Error('network');
          mocked(postDATA).mockRejectedValueOnce(mError);
          const dispatchedActions: any[] = [];
          await runSaga(
            {
              dispatch: (action) => dispatchedActions.push(action),
              getState: () => ({
                appState: {},
                appData: { userDetails: { name: 'mock-name' } },
              }),
            },
            onSaveDATA,
          ).toPromise();
          expect(postDATA).toBeCalledWith({ body: { name: 'mock-name' } });
          expect(dispatchedActions).toEqual([
            { type: 'SHOW_STATUS_MESSAGE', payload: { messageContent: 'Saving Your Data' } },
            { type: ActionTypes.CRITICAL_ERROR_OCCURED, payload: mError },
          ]);
        });
      });
    
      describe('save_on_change', () => {
        test('should wait for every SAVE_DATA_REQ action and call onSaveDATA', () => {
          const gen = save_on_change();
          expect(gen.next().value).toEqual(takeEvery(ActionTypes.SAVE_DATA_REQ, onSaveDATA));
          expect(gen.next().done).toBeTruthy();
        });
      });
    });
    

    unit test results with coverage report:

     PASS  src/stackoverflow/62952662/index.test.ts
      62952662
        onSaveDATA
          ✓ should save data (6 ms)
          ✓ should handle error if postDATA error (2 ms)
        save_on_change
          ✓ should wait for every SAVE_DATA_REQ action and call onSaveDATA (1 ms)
    
    ------------|---------|----------|---------|---------|-------------------
    File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ------------|---------|----------|---------|---------|-------------------
    All files   |      95 |      100 |   83.33 |   93.75 |                   
     index.ts   |     100 |      100 |     100 |     100 |                   
     service.ts |      50 |      100 |       0 |      50 | 2                 
    ------------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       3 passed, 3 total
    Snapshots:   0 total
    Time:        2.928 s, estimated 3 s