0

I am trying to test a reducer but not sure how to. the tutorials are different to how the code looks like so not sure how to implement.

actions/audit.js

   import axios from 'axios';

    export const FETCH_SOX_APPROVED_COMMS = 'fetch_sox_approved_comms';

    export async function fetchSoxApprovedComms(page, size, where, sort) {
        const request = await axios.get(`/api/user/report/sox/approved/comms/format/json?quiet=1&page=` + page + `&size=` + size + `&where=` + JSON.stringify(where) + `&sort=` + sort);
        return {
            type: FETCH_SOX_APPROVED_COMMS,
            payload: request
        };
    }

    export const FETCH_SOX_APPROVED_COMMS_COUNT = 'fetch_sox_approved_comms_count';

    export async function fetchSoxApprovedCommsCount(where) {
        const request = await axios.get(`/api/user/report/sox/approved/comms/count/format/json?quiet=1&where=` + JSON.stringify(where));
        return {
            type: F

ETCH_SOX_APPROVED_COMMS_COUNT,
        payload: request
    };
}

reducers/reducer_audit.js

import { 
    FETCH_SOX_APPROVED_COMMS, 
    FETCH_SOX_APPROVED_COMMS_COUNT
} from '../actions/audit';

export default function(state = {}, action) {
    switch (action.type) {
    case FETCH_SOX_APPROVED_COMMS:
    case FETCH_SOX_APPROVED_COMMS_COUNT:
        if ( typeof action.payload.data !== 'object'){
            return Object.assign({}, state, {
                error: {
                    code: "AUTHENTICATION",
                    message: 'Your session has expired',
                    action
                }
             });
        }else if (action.payload.data.header.error) {
            return Object.assign({}, state, {
                error: {
                    code: "INVALID ACTION",
                    message: action.payload.data.header.message,
                    action
                }
             });

       } else {
           return action.payload.data.body.recordset.record;
       }
    default:
        return state;
    }
}

tests/jest/reducers/reducer_audit.test.js

import reducer from '../../../app/reducers/reducer_audit';
import * as actions from '../../../app/actions/audit';
import expect from 'expect';

describe('audit reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toEqual({});
  });

  it('should handle FETCH_SOX_APPROVED_COMMS_COUNT', () => {
      const expected = {
          type: actions.FETCH_SOX_APPROVED_COMMS_COUNT
        };
    // it's empty on purpose because it's just starting to fetch posts
    expect(reducer({}, expected)).toEqual({});
  });

  it('should handle FETCH_SOX_APPROVED_COMMS');
});

the error i receive is

 audit reducer
    ✓ should return the initial state (2ms)
    ✕ should handle FETCH_SOX_APPROVED_COMMS_COUNT (7ms)
    ○ skipped 1 test

  ● audit reducer › should handle FETCH_SOX_APPROVED_COMMS_COUNT

    TypeError: Cannot read property 'data' of undefined

       8 |      case FETCH_SOX_APPROVED_COMMS:
       9 |      case FETCH_SOX_APPROVED_COMMS_COUNT:
    > 10 |              if ( typeof action.payload.data !== 'object'){
      11 |                      return Object.assign({}, state, {
      12 |                 error: {
      13 |                     code: "AUTHENTICATION",

      at Object.<anonymous>.exports.default (app/reducers/reducer_audit.js:10:30)
      at Object.<anonymous> (tests/jest/reducer/reducer_audit.test.js:15:9)

I have tried to follow this tutorial https://medium.com/@netxm/testing-redux-reducers-with-jest-6653abbfe3e1

In my action class the following is needed because my app goes through SSO and if the user does not have authenticated session it returns an html page

if ( typeof action.payload.data !== 'object'){
            return Object.assign({}, state, {
                error: {
                    code: "AUTHENTICATION",
                    message: 'Your session has expired',
                    action
                }
             });
        }

UPDATE

i added actions and expected but i don't understand how this works if i am specifying the data i am meant to receive and also matching the output.

    import reducer from '../../../app/reducers/reducer_audit';
    import * as actions from '../../../app/actions/audit';
    import expect from 'expect';

    describe('audit reducer', () => {
      it('should return the initial state', () => {
        expect(reducer(undefined, {})).toEqual({});
      });

      it('should handle FETCH_SOX_APPROVED_COMMS_COUNT', () => {
          const expected = {
                    id: 1,
                    name: 'testname'
                  };

          let action = {
                  type: actions.FETCH_SOX_APPROVED_COMMS_COUNT,
                  payload: {
                    data: {
                      header: {
                        error: null
                      },
                      body: {
                        recordset: {
                          record: {
                            id: 1,
                            name: 'testname'
                          }
                        }
                      }
                    }
                  }
                }
        expect(reducer({}, action)).toEqual(expected);
      });

      it('should handle FETCH_SOX_APPROVED_COMMS', () => {
          const expected = {
                  'approver_email':'',
                  'id':1,
                  'sox_approved':'',
                  'sox_submission':'',
                  'submitted_by':''
                  };

          let action = {
                  type: actions.FETCH_SOX_APPROVED_COMMS,
                  payload: {
                    data: {
                      header: {
                        error: null
                      },
                  body: {
                    recordset: {
                      record: {
                          'approver_email':'',
                          'id':1,
                          'sox_approved':'',
                          'sox_submission':'',
                          'submitted_by':''
                      }
                    }
                  }
                }
              }
            }
    expect(reducer({}, action)).toEqual(expected);
  });
});

If i am manually passing the action result and then manually passing expected how does this get tested? i understand i need to manually pass in expected but i don't understand why i have to input the resultset into action as well. I would have thought my action.fetchSoxApprovedCommsCount i would have to provide the params and expect the expected? is there any need to import actin file?

I have added minimal code here https://codesandbox.io/s/olwyjrwnjz

shorif2000
  • 2,582
  • 12
  • 65
  • 137
  • Jemi is correct, the action being passed to the test does not have a payload. In addition to that issue, it looks like the payload for FETCH_SOX_APPROVED_COMMS_COUNT can either contain nothing indicating an authentication error, or an error, or the records from which you then pull the count. I would recommend that the authentication and error handling logic be moved into fetchSoxApprovedCommsCount and that the action be dispatched with only the count in the payload. You can create other actions for authentication issues and errors and dispatch those if they occur. – Brian Adams Aug 03 '18 at 16:43
  • @brian-lives-outdoors how can i add other actions to handle the error or no data – shorif2000 Aug 06 '18 at 15:24
  • [This section](https://redux.js.org/basics/reducers#splitting-reducers) describes what I am talking about. There should be one reducer that handles the part of the state with the count and FETCH_SOX_APPROVED_COMMS_COUNT should only ever update it with a payload that is just the count There should be another reducer that handles the part of the state with the error. It should have its own actions that are able to set the error part of the state. fetchSoxApprovedCommsCount should dispatch an error action if an error occurs, or FETCH_SOX_APPROVED_COMMS_COUNT with the count. – Brian Adams Aug 06 '18 at 18:38
  • I couldn't really understand how i would handle an error action reading that. i have put sample code on codesandbox. link is in question. would you be able to show me a sample of how i would handle the 3 errors. 1. returns html so no SSO session exists. 2. return nothing 3. return a valid error – shorif2000 Aug 07 '18 at 09:20

1 Answers1

1

You're getting the error because you've forgotten to include a payload in your test action.

You're passing the following action to the reducer.

{
  type: actions.FETCH_SOX_APPROVED_COMMS_COUNT
}

The reducer attempts to read action.payload.data, which results in an error because action.payload is undefined.

You have to pass in an action with some realistic data. For example:

const action = {
  type: actions.FETCH_SOX_APPROVED_COMMS_COUNT,
  payload: {
    data: {
      header: {
        error: null
      },
      body: {
        recordset: {
          record: {
            id: 1,
            name: 'testname'
          }
        }
      }
    }
  }
}

After which you should expect the value to be appropriate. In my example case

expect(reducer({}, action)).toEqual({ id: 1, name: 'testname' })

would pass.

Jemi Salo
  • 3,401
  • 3
  • 14
  • 25
  • I don't know what you're still confused about. Could you elaborate on your update? Please frame it as a question. – Jemi Salo Aug 06 '18 at 16:39
  • Reducers take a state and an action and return a new state. That's all a reducer does and that's all you should focus on when testing reducers. Reducer tests should almost exclusively follow the pattern of 1. define state and action. 2. expect return value of reducer with given inputs. Logic for creating and dispatching actions is in some other files of your project. Save testing that functionality for those file's tests. – Jemi Salo Aug 07 '18 at 09:08
  • but what will the inputs of the reducers be? – shorif2000 Aug 07 '18 at 09:18
  • would this be the correct way to test the action? https://stackoverflow.com/questions/51656720/how-to-match-a-returned-promise-in-using-jest-with-redux-action – shorif2000 Aug 07 '18 at 09:45
  • 1. Inputs should be like the inputs you expect to receive. If you don't know what they are like, check your redux dev tools in your browser. 2. Unit testing a reducer should focus on states and actions only, like I said earlier. You can separately write tests to assert your services dispatch the correct actions. You're trying to test both the code that dispatches the action and the reducer at the same time, which is neither a clean or easy way of writing tests. – Jemi Salo Aug 08 '18 at 12:13