React, Jest, Testing Library
Arrange
変数定義
jest.Mocked<Source>
利用でオブジェクトにプロパティ追加時、テストコードの修正箇所が限定できて良さそう。
interface DataModelA {
id: number
value: string
someOtherProp1: string
someOtherProp2: string
}
テストコードでは、コンポーネントで必要なプロパティのみを設定する。idとnameしか参照しないなら以下。
const DUMMY_MAIN_SERVICE = {
id: 1, name: 'dummy',
} as jest.Mocked<DataModelA>
そうすると、DataModelA
へsome-other-props-3
が増えてもそれが不要なコンポーネントのテストは修正不要。
Custom Hooksのモック
const mock = jest.fn()
jest.mock(`${Custom Hookへのパス importのfrom指定と同じ}`, () => ({
<exportしてる変数名>: (): jest.Mock => mock,
}))
テストが実行されるより前にモックする必要があるため、テストファイルの先頭の方に書くと良さそう。
Note: expect(mock).toHaveBeenCalledWith('testParam')
でテストできる。
Act
コンポーネントをrenderするにはReact Testing Libraryのrender関数が利用可能。
render対象のコンポーネントに非同期処理が含まれるなら、render関数の実行結果RenderResultのメソッドfindByRoleなどで、テスト対象の表示を待つことができる。
たとえば以下のAppコンポーネントにデータfetchなど非同期処理が含まれていて、それが完了するまで「テスト対象ボタン」が表示されない場合 await renderResult.findByRole('button', {name: /テスト対象ボタン/})
すれば、これより後は「テスト対象ボタン」が表示された状態をassertすることができる。
const renderResult = render(<App />)
const button = await renderResult.findByRole('button', {name: /テスト対象ボタン/})
fireEvent.click(button)
input要素のchangeValue
fireEvent.change(getByRole('textbox', {name: '本文'}), {target: {value: 'テスト本文更新'}})
要素が消えることを待つ
waitForElementToBeRemoved
it('キャンセルボタンクリックでダイアログを非表示', async () => {
const {getByRole, queryByText, queryByTestId} = subject({})
fireEvent.click(getByRole('button', {name: '登録'}))
fireEvent.click(getByRole('button', {name: 'キャンセル'}))
await waitForElementToBeRemoved(() => queryByText('登録します。 よろしいですか?'))
expect(await queryByTestId('register-confirm-dialog')).not.toBeInTheDocument()
})
Assert/Expect
要素が存在しないこと
expect(screen.queryByTestId('testIdOfTestTarget')).not.toBeInTheDocument()
要素にテキストがあること
const {getByText} = render(<SutComponent><>存在すべきテキスト</></SutComponent>)
// or
const {getByText} = within(screen.getByTestId('testIdOfTestTarget'))
expect(getByText('存在すべきテキスト')).toBeInTheDocument()
<li>の要素が正しい並び順であること
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
があるとき
const {getAllByRole} = within(screen.getByTestId('testIdOfTestTarget'))
const items = getAllByRole('listitem')
expect(items).toHaveLength(2)
expect(items[0]).toHaveTextContent('アイテム1')
expect(items[1]).toHaveTextContent('アイテム2')
Parameterized Test
it.each`
items | description
${undefined} | ${'undefined'}
${[]} | ${'空配列'}
${[new Item(1, 'アイテム1')]} | ${'アイテムが1つ'}
`('アイテムが$descriptionの場合はアイテム情報を表示しない', ({items}) => {
render(<Component items={items} >)
// expect
})
要素のリンクが正しいこと
<a href="https://www.google.co.jp/" target="_blank" rel="noopener noreferrer" class="css-bqp9i"><div direction="row" class="css-1j132cc">Googleへのリンク<img src="data:image/png;base64,iVB...(省略)=" alt="whiteArrowForwardIcon" class="css-q7z9e3"></div></a>
この要素は name Name "利用規約 whiteArrowForwardIcon"
として見えてるので、以下の通り正規表現でマッチさせると良い。
import {render, screen} from '@testing-library/react'
expect(screen.getByRole('link', {name: /Googleへのリンク/})).toHaveAttribute('href', 'https://www.google.co.jp/')
以下のPriorityに従いgetByRoreを積極的に使っていきたい。
getByRole利用することでラベル設定間違えていたときにgetByRoleで要素が取れなくてミスに気付けた。テスト expect(getByRole('textbox', {name: 'コメント'})).toHaveValue('テストコメント')
書いたら Unable to find an accessible element with the role "text" and name "コメント"
でテストが失敗した。そのときにエラーコンソールで出力された内容が以下。nameが空文字列になっていた。対象のtsx定義確認したところ htmlFor
指定値が誤っていた。
textbox:
Name "":
<textarea
class="..."
id="content-comment"
name="comment"
/>
input要素のvalueが正しいこと
getByRole
、getByLabelText
や getByPlaceholderText
が使えればそれを使って要素を取得。以下みたいに使えない場合はたとえば getByDisplayValue
で取得。
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input ..."
id="date-input"
type="date"
value="2020-01-01"
/>
const {getByDisplayValue} = render(<DatetimePicker datetime={'2020/01/01 00:00'} />)
expect(getByDisplayValue('2020-01-01')).toBeInTheDocument()
input要素のonChangeでコールバックされる関数の引数を検査
it('日付を変更したとき、更新された日付でonChangeを呼び出す', async () => {
const fn = jest.fn()
const {getByDisplayValue} = render(
<DatetimePicker datetime={'2020/01/01 12:00'} onChange={fn}/>)
const inputElement = (await getByDisplayValue('2020-01-01')) as HTMLInputElement
fireEvent.change(inputElement, {target: {value: '1900-01-01'}})
expect(fn).toHaveBeenCalledWith('1900/01/01 12:00')
})
Note
テストに直接は無関係なこと。
useReducer
オブジェクトの一部を更新するstateはuseReducer利用すると便利。
const [content, setContent] = useReducer((a: ContentDetail, b: Partial<ContentDetail>) => ({...a, ...b}), a)
// ...
onChange={(e): void => setContent({comment: e.target.value})}