RoadMovie

write down memos or something I found about tech things

【Jest, enzyme】 useSelectorやuseDispatchを使ったFunctionComponentをうまくテストする

ReactのテストでJestやenzymeを使うことは多いと思うのですが、React v16.8以降でFunctionComponentでの書き方を使う機会が増えたにもかかわらず、useSelectorやuseDispatchあたりをclassでなくFunctionComponentで使ったテストケースがあまり見つからず困ったのでサンプルをいくつか書いてみます。

typescriptで書いてますが、使わない場合はよしなに読み替えていただければと思います。

The product code

かなり単純化したものになりますが、下記のようなコードに対してテストを書きたい場合です。

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import * as actions from './actions'

const UserList: React.FC = () => {
  const dispatch = useDispatch()
  const users = useSelector(state => state.reducer.users)
  const companies = useSelector(state => state.reducer.companies)

  const submit = (userId: number) => {
    dispatch(actions.postUser(userId))
  }

  return (
    <div>
      {users.map(user =>
        <div>
          <p>{user.name}</p>
          <button onClick={() => submit(user.id)}>submit</button>
        </div> 
      )}
    </div>
  )
}

Writing Test code with mocking

単純なのですが、いろいろハマるところがあったのでひとつずつ解いていきます。
※ やり方はここで書く以外にも何通りかあります

# UserList.spec.tsx

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { shallow } from 'enzyme'
import * as actions from './actions'

jest.mock('react-redux') // -> ① 
jest.mock('./actions')       // -> ②

まず、① react-redux をimportして jest.mock しておくのが最初のポイントです。② actionsもここでmockしておくと良いです。

const useSelector = useSelector as jest.mock // -> ③

useSelectorはここでmockしましょう。いよいよ具体的にテストを書いていきます。

describe('UserList', () => {
  afterEach(() => {
    jest.resetAllMocks() // -> ④
  })

  let wrapper
  beforeEach(() => {
    const users = [{ id: 1, name: 'userName' }] as User[]
    const companies = [{ id: 1, divisionName: 'R&D' }]
    
    useSelectorMock.mockReturnValueOnce(users) // -> ⑤
    useSelectorMock.mockReturnValueOnce(companies) // -> ⑥
    useDispatch.mockReturnValue(jest.fn()) // -> ⑦

    wrapper = shallow(<UserList />)
  })

  it('renders view', () => {
    expect(wrapper.text()).toEqual(
      expect.stringContaining('userName')
    )
  })
})

これでテストには通ると思います。④でafterEachとしてmock化の解除を行います。beforeEachの中が需要で、⑤⑥でReturnValueOnceを設定することで、それぞれuseSelectorの返すべき値をusersとcompaniesに振り分けています。simulateなどで再レンダリングさせる際は、このペアをもう一度事前に登録しておくことを忘れないようにしてください。⑦はuseDispatchのmockです。

Additionally

ついでに、actionのテストも簡単に見てみましょう。

describe('click the button', () => {
  let spy
  beforeEach(() => {
    // -> actionsは最初にmock化してあります
    spy = jest.spyOn(actions, 'postUser')

    // -> 先程のbeforeEachが効いているscope内として、
    // あと2つ再レンダリング用に書きます
    useSelectorMock.mockReturnValueOnce(users)
    useSelectorMock.mockReturnValueOnce(companies)
  })

  it('calls api', () => {
    wrapper.find('button').simulate('click')
    expect(spy).toHaveBeenCalled()
  })
})

こんな感じです。 import * as actions のmock化とかも地味に躓きましたが、上記のような感じで上手く行けると思います。他にも act 周りとかで細かいtipsはあるのですが、それはまたの機会に。enzymeのshallow, mountになれるのもjestテストを快適に書くポイントですよね。