UGA Boxxx

つぶやきの延長のつもりで、知ったこと思ったこと書いてます

【Redux】React + Redux のテストを考える

React + Redux アプリケーションのテストを考えているときに、reduxのテストの書き方のドキュメントと、丁度以下の記事を目にしたので、自身のプロジェクトに当てはめてみる

recruit-tech.co.jp

上の記事で挙げられているテストの対象は以下

  1. Action Creator
  2. Reducer
  3. Selector
  4. Middleware
  5. API通信処理

Action Creator

自身のプロジェクトではAction CreatorはActionTypeと引数をそのまま渡す程度の薄い関数のため書くべきか悩むが、メリットはあるため優先度を下げて対応を行うことにする

書くとしたらこんな感じ

export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}
import * as actions from '../../actions/TodoActions'
import * as types from '../../constants/ActionTypes'

describe('actions', () => {
  it('should create an action to add a todo', () => {
    const text = 'Finish docs'
    const expectedAction = {
      type: types.ADD_TODO,
      text
    }
    expect(actions.addTodo(text)).toEqual(expectedAction)
  })
})

Reducer

Reducer内にロジックを持っているため厚くテストを書いておきたい

以下に対するテストを忘れすに行う

  • ロジック
  • 正常系
  • 異常系
  • 境界値
import { ADD_TODO } from '../constants/ActionTypes'

const initialState = [
  {
    text: 'Use Redux',
    completed: false,
    id: 0
  }
]

export default function todos(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        {
          id: state.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1,
          completed: false,
          text: action.text
        },
        ...state
      ]

    default:
      return state
  }
}
import reducer from '../../structuring-reducers/todos'
import * as types from '../../constants/ActionTypes'

describe('todos reducer', () => {
  it('should return the initial state', () => {
    expect(reducer(undefined, {})).toEqual([
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })

  it('should handle ADD_TODO', () => {
    expect(
      reducer([], {
        type: types.ADD_TODO,
        text: 'Run the tests'
      })
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 0
      }
    ])

    expect(
      reducer(
        [
          {
            text: 'Use Redux',
            completed: false,
            id: 0
          }
        ],
        {
          type: types.ADD_TODO,
          text: 'Run the tests'
        }
      )
    ).toEqual([
      {
        text: 'Run the tests',
        completed: false,
        id: 1
      },
      {
        text: 'Use Redux',
        completed: false,
        id: 0
      }
    ])
  })
})

Selector

Selectorを積極的に使っていなかったので、これを機に使うようにする

Middleware

権限周りを扱うかなり重要なMiddlewareがあるため、その辺りを重点的にテストする

const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}
const create = () => {
  const store = {
    getState: jest.fn(() => ({})),
    dispatch: jest.fn()
  }
  const next = jest.fn()

  const invoke = action => thunk(store)(next)(action)

  return { store, next, invoke }
}

it('passes through non-function action', () => {
  const { next, invoke } = create()
  const action = { type: 'TEST' }
  invoke(action)
  expect(next).toHaveBeenCalledWith(action)
})

it('calls the function', () => {
  const { next, invoke } = create()
  const fn = jest.fn()
  invoke(fn)
  expect(next).toHaveBeenCalled()
})

it('passes dispatch and getState', () => {
  const { store, invoke } = create()
  invoke((dispatch, getState) => {
    dispatch('TEST DISPATCH')
    getState()
  })
  expect(store.dispatch).toHaveBeenCalledWith('TEST DISPATCH')
  expect(store.getState).toHaveBeenCalled()
})

API通信処理

Fetch処理は外部ライブラリで行っているため書かない

参考

https://redux.js.org/recipes/writing-tests#writing-tests

https://testingjavascript.com/