💭

ReactでJestを使ったテストをする場合にuseStateをmockしたいんだけど

2022/03/31に公開

こんなかんじで Item を表示する <ItemList /> があったとする。

たまに、コンポーネント内で useState を使わず、外から注入したいケースがあると思う。
(あまりお作法的によくないのかもしれないけど、親コンポーネントから state を注入したいケースは一定ありそうじゃない?)

import React, { useState } from 'react'

interface Item {
  id: number
  name: string
}

type ItemProps = {
  item: Item
  orderBack: () => void
  orderForward: () => void
}
const Item: React.FC<ItemProps> = ({ item, orderBack, orderForward }) => {
  const [current, setCurrent] = useState<Item>(item)

  return (
    <ItemContainer>
      <label htmlFor={`Item${current.id}_Input_Id`}>ID</label>
      <input type="text" id={`Item${current.id}_Input_Id`} value={current.id} data-testid="Item_Input_Id" readonly />
      <label htmlFor={`Item${current.name}_Input_Name`}>Name</label>
      <input type="text" id={`Item${current.name}_Input_Name`} defaultValue={current.name} onBlur={(e) => setCurrent({ ...current, name: e.target.value })} data-testid="Item_Input_Name" />
      <button type="button" onClick={orderBack} data-testid="Item_OrderBack">🔼</button>
      <button type="button" onClick={orderForward} data-testid="Item_OrderForward">🔽</button>
    </ItemContainer>
  )
}

type Props = {
  items: Item[]
  setItems: (items: Item[]) => void
}
export const ItemList: React.FC<Props> = ({ items, setItems }) => {
  // cf. https://www.infoscoop.org/blogjp/2012/05/02/js-array-splice/
  const orderBack = (i) => {
    const updated = items
    updated.splice(i, 2, updated[i+1], updated[i]);
    setItems(updated)
  }
  const orderForward = (i) => {
    const updated = items
    updated.splice(i-1, 2, updated[i], updated[i-1]);
    setItems(updated)
  }

  return (
    <Container>
      {items.length > 0 &&
        items.map((item, i) => (
          <Item item={item} orderBack={() => orderBack(i)} orderForward={() => orderForward(i)} />
        ))}
    </Container>
  )
}

その際に、 state が更新されたことをテストしたい場合があるだろう。
その場合、以下のように jest.fn()render() の返り値に含まれる rerender() を使ってあげるとよい。
そうすると、以下の場合だと setItemsuseState を用いて更新したかのように振る舞ってくれるはずだ。

import { fireEvent, render, screen } from '@testing-library/react'
import { ItemList } from 'components/ItemList' 
import React from 'react'

describe('<ItemList />', () => {
  test('item\'s order back button should be sorted collectly', () => {
    const items = [
      { id: 1, name: 'アイテム1' },
      { id: 5, name: 'アイテム5' },
      { id: 100, name: 'アイテム100' },
    ]
    const sortedItems = [
      { id: 5, name: 'アイテム5' },
      { id: 1, name: 'アイテム1' },
      { id: 100, name: 'アイテム100' },
    ]

    const setItems = jest.fn((items) => {
      rerender(<ItemList items={items} setItems={setItems} />)
    })
    const { rerender } = render(<ItemList items={items} setItems={setItems} />)

    const orderBackButtons = screen.getAllByTestId('Item_OrderBack')
    fireEvent.click(orderBackButtons[0])
 
    const itemInputIds = screen.getAllByTestId('Item_Input_Id') as HTMLInputElement[]
    const itemInputNames = screen.getAllByTestId('Item_Input_Name') as HTMLInputElement[]

    itemInputIds.forEach((input, idx) => {
      expect(input).toHaveDisplayValue(sortedItems[idx].id)
    })
    itemInputNames.forEach((input, idx) => {
      expect(input).toHaveDisplayValue(sortedItems[idx].name)
    })
  })
})

@testing-library/react-hooksの方が楽な気もするけど

@testing-library/react-hooks ってのもいるっぽいので、そちらを使って hooks のテストを書くこともできそう...? 👀
cf. React Hooks Testing | Kumasan

とはいえ、最近読み進めてる書籍「Software Engineering at Google」を読んでると「モッキングフレームワークは強力すぎるから用法容量を守って正しく使いなよ」的なことを言ってたりするので、必要な振る舞いをrerender()とか使って必要な部分だけ愚直にモックしてあげた方がよいのかなあ、なんてことも思う。まあ、モッキングフレームワークを使った方が考えなくて済むし楽なんだけどね。

参考

Discussion