Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to unit test reduxsauce?

I am using reduxsauce library for redux store, and I want to unit test a single redux store in it. The redux file:

import { createReducer, createActions } from 'reduxsauce'
import Immutable from 'seamless-immutable'

/* ------------- Types and Action Creators ------------- */

const { Types, Creators } = createActions({
  getLanguage: [],
  setLanguage: ['language']
})

export const LanguageTypes = Types
export default Creators

/* ------------- Initial State ------------- */

export const INITIAL_STATE = Immutable({
  language: "en"
})

/* ------------- Reducers ------------- */


export const getLanguage = (state: Object, {}: Object) => {
    return state.merge({})
}

export const setLanguage = (state: Object, { language }: Object) => {
    return state.merge({ language })
}

/* ------------- Hookup Reducers To Types ------------- */

export const reducer = createReducer(INITIAL_STATE, {
  [Types.SET_LANGUAGE]: setLanguage,
  [Types.GET_LANGUAGE]: getLanguage,
})

The test:

import * as actions from '../../../redux/LanguageRedux'
import * as types from '../../../redux/LanguageRedux'

describe('Language redux ', () => {
  it('should have default language ', () => {
    expect(actions.INITIAL_STATE.language).toEqual("en")
  }),
  it('should be able to set the language', () => {
    // I know the calls below are not tests but still its relevant with the error
    actions.getLanguage()
    actions.setLanguage()
  })
})

Error:

● Language redux  › should be able to set the language

    TypeError: Cannot destructure 'undefined' or 'null'.

      21 |
      22 |
    > 23 | export const getLanguage = (state: Object, {}: Object) => {
         |                            ^
      24 |     return state.merge({})
      25 | }
      26 |

      at Object.getLanguage (src/redux/LanguageRedux.js:23:28)
      at Object.getLanguage (src/__tests__/src/redux/LanguageRedux.js:9:13)

Now, I have the store configured in a different file but reducers is combined in a different file:

import { combineReducers } from 'redux'
import configureStore from './CreateStore'

import rootSaga from '../sagas'

export default () => {
    /* ------------- Assemble The Reducers ------------- */
    const rootReducer = combineReducers({
        language: require('./LanguageRedux').reducer
    })

    return configureStore(rootReducer, rootSaga)
}

Any one has a clue as to how could I test the redux actions etc. With normal redux I could find many articles but with reduxsauce library I cant seem to find anything. Any clues please?

like image 342
Maverick Avatar asked Mar 19 '19 13:03

Maverick


People also ask

How do you write a unit test for reducers?

const [state, dispatch] = useReducer(reducer, reducer()); Suppose you can't call your reducers without arguments (for example because you're using Redux Toolkit). In that case, you can write this test by passing undefined and an empty object.

How to test Redux application?

Redux can be tested with any test runner, since it's just plain JavaScript. One common option is Jest, a widely used test runner that comes with Create-React-App, and is used by the Redux library repos. If you're using Vite to build your project, you may be using Vitest as your test runner.


1 Answers

What is being tested

LanguageRedux.js has the following exports:

  • LanguageTypes - a map of the action types
  • Creators - a map of the action creators
  • INITIAL_STATE - the initial state of the app
  • getLanguage and setLanguage - the reducer functions
  • reducer - the redux reducer

I recommend importing everything with the expected identifiers like this:

import Creators, {
  LanguageTypes,
  INITIAL_STATE,
  getLanguage,
  setLanguage,
  reducer
 } from '../../../redux/LanguageRedux';

Note: It looks like the getLanguage action is unnecessary since it does nothing to the state (if the app is getting the language it should just read it from the state), but I'll leave it in there since it is in the question code.


LanguageTypes

LanguageTypes is just a map of the action types to their associated string value:

it('should export the expected action types', () => {
  expect(LanguageTypes).toEqual({
    GET_LANGUAGE: 'GET_LANGUAGE',
    SET_LANGUAGE: 'SET_LANGUAGE'
  });  // Success!
});

Creators

Creators is a map of the action creators.

Each action creator is a pure function that generates an action object based on the parameters given:

describe('Creators', () => {

  describe('getLanguage', () => {

    it('should return the expected action', () => {
      expect(Creators.getLanguage()).toEqual({
        type: LanguageTypes.GET_LANGUAGE
      });
    });

    it('should ignore extra args', () => {
      expect(Creators.getLanguage('extra arg')).toEqual({
        type: LanguageTypes.GET_LANGUAGE
      });
    });

  });

  describe('setLanguage', () => {

    it('should return the expected action when passed nothing', () => {
      expect(Creators.setLanguage()).toEqual({
        type: LanguageTypes.SET_LANGUAGE
      });  // Success!
    });

    it('should return the expected action when passed a language', () => {
      expect(Creators.setLanguage('en')).toEqual({
        type: LanguageTypes.SET_LANGUAGE,
        language: 'en'
      });  // Success!
    });

    it('should ignore extra args', () => {
      expect(Creators.setLanguage('es', 'extra arg')).toEqual({
        type: LanguageTypes.SET_LANGUAGE,
        language: 'es'
      });  // Success!
    });

  });

});

INITIAL_STATE

INITIAL_STATE is simply the initial state object the app starts with:

it('should set the initial state ', () => {
  expect(INITIAL_STATE).toEqual({ language: "en" });  // Success!
});

Reducer functions

getLanguage and setLanguage are reducer functions, meaning they are pure functions that return a new state based on the existing state and action they are given:

describe('reducers', () => {

  describe('getLanguage', () => {

    it('should do nothing (probably should not be an action)', () => {
      expect(getLanguage(INITIAL_STATE, {})).toEqual(INITIAL_STATE);  // Success!
    });

    it('should ignore extra args', () => {
      expect(getLanguage(INITIAL_STATE, { extra: 'arg' })).toEqual(INITIAL_STATE);  // Success!
    });

  });

  describe('setLanguage', () => {

    it('should set the language', () => {
      expect(setLanguage(INITIAL_STATE, { language: 'es' })).toEqual({
        language: 'es'
      });  // Success!
    });

    it('should ignore extra args', () => {
      expect(setLanguage(INITIAL_STATE, { language: 'fr', extra: 'arg' })).toEqual({
        language: 'fr'
      });  // Success!
    });

  });

});

Note that testing reducer functions with reduxsauce is even easier than testing standard redux reducers since they will only be called for actions they are designed to handle.


reducer

reducer is the redux reducer and its job is to route actions to the corresponding reducer function and return the resulting state:

describe('reducer', () => {

  it('should return initial state if passed nothing', () => {
    expect(reducer()).toEqual(INITIAL_STATE);  // Success!
  });

  it('should route GET_LANGUAGE to getLanguage', () => {
    expect(reducer(INITIAL_STATE, Creators.getLanguage())).toEqual(INITIAL_STATE);  // Success!
  });

  it('should route SET_LANGUAGE to setLanguage', () => {
    expect(reducer(Immutable({ language: 'es' }), Creators.setLanguage('fr'))).toEqual({
      language: 'fr'
    });  // Success!
  });

});

Note: there are a few different ways that reducer can be tested. The above approach passes the state and actions all the way through the reducer functions. It is thorough, but also has a lot of overlap with the reducer function tests above.

The most basic alternative is to spy on createReducer and simply verify that it was called with the expected INITIAL_STATE and mapping object.

The approach halfway between that and the full approach above is to mock the reducer functions, pass reducer various actions, and verify that the correct reducer function was called. This is probably the ideal approach but it is difficult to implement the way the code is currently written since createReducer runs as soon as the code is imported and captures references to the local functions setLanguage and getLanguage. If you wanted to use this approach then the easiest way to do it would be to move reducer to its own module so you could mock the reducer functions before importing reducer into your test code.

like image 135
Brian Adams Avatar answered Oct 18 '22 11:10

Brian Adams