📜 ⬆️ ⬇️

Testing redux

Using the example of a regular blog (getting data from post-comments from the API), I will demonstrate how I cover a redux-layer with tests. Sources are available here .


Instead of separate actions and reducers, I use the ducks-pattern , which greatly simplifies both the development and testing of redux in the application. I also use a very useful tool - redux-act, but it’s important to add exclusively in the description field of the createAction () method: numbers, capital letters and underscores ( proof ).


To begin with, the test for a simple "action creator" of type { type, payload } - app.setLoading ():


 // src/ducks/app.js import { createAction, createReducer } from 'redux-act' export const REDUCER = 'APP' const NS = `${REDUCER}__` export const initialState = { isLoading: false, } const reducer = createReducer({}, initialState) export const setLoading = createAction(`${NS}SET`) reducer.on(setLoading, (state, isLoading) => ({ ...state, isLoading })) export default reducer 

Minimum for the first run of the test:


 // src/ducks/__tests__/app.test.js import thunk from 'redux-thunk' import configureMockStore from 'redux-mock-store' import { setLoading } from '../app' import reducer from '..' const middlewares = [thunk] const mockStore = configureMockStore(middlewares) describe('sync ducks', () => { it('setLoading()', () => { let state = {} const store = mockStore(() => state) store.dispatch(setLoading(true)) const actions = store.getActions().map(({ type, payload }) => ({ type, payload })) console.log(actions) // ...   -    }) }) 

I copy from the console the value for expectedActions:


  const expectedActions = [{ type: 'APP__SET', payload: true }]; expect(actions).toEqual(expectedActions); 

I apply actions (with data in payload for each action) to the root reducer obtained from combineReducers ():


  actions.forEach(action => { state = reducer(state, action) }) expect(state).toEqual({ ...state, app: { ...state.app, isLoading: true }, }) 

It should be clarified that the store is created with the mockStore(() => state) callback function - to provide the current state in getState() calls inside side effects redux-thunk.

That's it, the first test is ready!


Further more interesting, you need to cover the side effects of post.load ():


 // src/ducks/post.js import { createAction, createReducer } from 'redux-act' import { matchPath } from 'react-router' import axios from 'axios' import { load as loadComments } from './comments' export const REDUCER = 'POST' const NS = `${REDUCER}__` export const initialState = {} const reducer = createReducer({}, initialState) const set = createAction(`${NS}SET`) reducer.on(set, (state, post) => ({ ...state, ...post })) export const load = () => (dispatch, getState) => { const state = getState() const match = matchPath(state.router.location.pathname, { path: '/posts/:id' }) const id = match.params.id return axios.get(`/posts/${id}`).then(response => { dispatch(set(response.data)) return dispatch(loadComments(id)) }) } export default reducer 

Although comments.load () is also exported, but testing it separately does not make much sense, because it is used only inside our post.load ():


 // src/ducks/comments.js import { createAction, createReducer } from 'redux-act' import axios from 'axios' export const REDUCER = 'COMMENTS' const NS = `${REDUCER}__` export const initialState = [] const reducer = createReducer({}, initialState) const set = createAction(`${NS}SET`) reducer.on(set, (state, comments) => [...comments]) export const load = postId => dispatch => { return axios.get(`/comments?postId=${postId}`).then(response => { dispatch(set(response.data)) }) } export default reducer 

Side Effect Test:


 // src/ducks/__tests__/post.test.js import thunk from 'redux-thunk' import configureMockStore from 'redux-mock-store' import axios from 'axios' import AxiosMockAdapter from 'axios-mock-adapter' import { combineReducers } from 'redux' import post, { load } from '../post' import comments from '../comments' const middlewares = [thunk] const mockStore = configureMockStore(middlewares) const reducerMock = combineReducers({ post, comments, router: (state = {}) => state, }) const axiosMock = new AxiosMockAdapter(axios) describe('sideeffects', () => { afterEach(() => { axiosMock.reset() }) it('load()', () => { const postResponse = { userId: 1, id: 1, title: 'title', body: 'body', } axiosMock.onGet('/posts/1').reply(200, postResponse) const commentsResponse = [ { postId: 1, id: 1, name: 'name', email: 'email@example.com', body: 'body', }, ] axiosMock.onGet('/comments?postId=1').reply(200, commentsResponse) let state = { router: { location: { pathname: '/posts/1', }, }, } const store = mockStore(() => state) return store.dispatch(load()).then(() => { const actions = store.getActions().map(({ type, payload }) => ({ type, payload })) const expectedActions = [ { type: 'POST__SET', payload: postResponse, }, { type: 'COMMENTS__SET', payload: commentsResponse }, ] actions.forEach(action => { state = reducerMock(state, action) }) expect(state).toEqual({ ...state, post: { ...state.post, ...postResponse }, comments: [...commentsResponse], }) }) }) }) 

I do not know how to do it better, but for the sake of initializing the router reducer, I had to rebuild the root reducer in reducerMock. Plus blende for two requests to axios. Return was added to store.dispatch () as well. wrapped in a promise; but there is an alternative - the done () callback function:


  it('', done => { setTimeout(() => { //... done() }, 1000) } 

And the rest of the test for the side effect is not more difficult than the test for a simple "action creator". Sources are available here .


')

Source: https://habr.com/ru/post/340706/


All Articles