💭
ReactでJestを使ったテストをする場合にuseStateをmockしたいんだけど
こんなかんじで 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()
を使ってあげるとよい。
そうすると、以下の場合だと setItems
が useState
を用いて更新したかのように振る舞ってくれるはずだ。
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