「動いていたはずの機能が、別の修正で壊れる」——これを機械に見張らせるのがテストです。
実務では、テストが書けるかどうかがレビューの通りやすさに直結します。この回で実際に書いて、走らせます。
何を入れるか
ViteのプロジェクトではVitestが標準的な選択です(Viteの設定をそのまま使えるため)。それに、Reactの部品を操作するためのTesting Libraryを足します。
npm install --save-exact -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom| パッケージ | 役目 |
|---|---|
| vitest | テストを走らせる本体 |
| @testing-library/react | コンポーネントを描画して、要素を探す |
| @testing-library/user-event | クリック・入力をユーザーに近い形で起こす |
| @testing-library/jest-dom | toBeInTheDocument()のような読みやすい検証を足す |
| jsdom | ブラウザの代わり(Node上にDOMを用意する) |
設定はvite.configに足すだけです。
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/setupTests.ts',
},
})import '@testing-library/jest-dom/vitest'package.jsonに"test": "vitest"を足せば準備完了です。
最初の1本
BirdCardのテストを書きます。ファイル名はテスト対象と同じ場所に.test.tsxで置くのが分かりやすい形です。
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router-dom'
import { describe, expect, it, vi } from 'vitest'
import BirdCard from './BirdCard.tsx'
import type { Bird } from '../types.ts'
const bird: Bird = {
id: 1,
name: 'シマエナガ',
emoji: '🐦',
area: '北海道',
rare: 3,
memo: '雪のようにまっ白な小鳥。',
}
describe('BirdCard', () => {
it('名前・すんでいる場所・レアバッジが出る', () => {
render(
<MemoryRouter>
<BirdCard bird={bird} isFavorite={false} onToggleFavorite={() => {}} />
</MemoryRouter>,
)
expect(screen.getByRole('heading', { name: 'シマエナガ' })).toBeInTheDocument()
expect(screen.getByText('北海道')).toBeInTheDocument()
expect(screen.getByText('レア')).toBeInTheDocument()
})
})3つのことをしています。
render(…)——コンポーネントを描画するscreen.getByRole(…)——ユーザーと同じ探し方で要素を見つけるexpect(…).toBeInTheDocument()——あることを確かめる
getByRoleで探す
要素の探し方はいくつかありますが、優先順位があります。
| 探し方 | 使うとき |
|---|---|
getByRole('button', { name: '…' }) | 第一候補。 ユーザーと同じ探し方 |
getByLabelText('名前') | フォームの入力欄 |
getByText('…') | 上2つで届かない表示テキスト |
getByTestId('…') | 最後の手段(data-testidを足す) |
getByRoleが推奨なのは、スクリーンリーダーと同じ見つけ方だからです。ここで見つからないなら、支援技術からも見つからない可能性が高い——テストがアクセシビリティの検査を兼ねます。
操作をテストする
押したときに何が起きるかを見ます。関数が呼ばれたかを確かめるのが基本形です。
it('星を押すとidつきでonToggleFavoriteが呼ばれる', async () => {
const onToggleFavorite = vi.fn() // 呼ばれたか記録する偽の関数
render(
<MemoryRouter>
<BirdCard bird={bird} isFavorite={false} onToggleFavorite={onToggleFavorite} />
</MemoryRouter>,
)
await userEvent.click(
screen.getByRole('button', { name: 'シマエナガをお気に入りにする' }),
)
expect(onToggleFavorite).toHaveBeenCalledWith(1)
})vi.fn()——呼ばれた回数と引数を覚えてくれる偽の関数await userEvent.click(…)——awaitを忘れると、押す前に検証が走ります
フォームも同じ書き方で検証できます。
it('名前が空なら追加されず、エラーが出る', async () => {
const onAdd = vi.fn()
render(<AddBirdForm onAdd={onAdd} />)
await userEvent.click(screen.getByRole('button', { name: 'ずかんに追加' }))
expect(screen.getByText('名前を入れてください')).toBeInTheDocument()
expect(onAdd).not.toHaveBeenCalled()
})入力チェックのような「動くけれど目で確認しにくい」ものほど、テストの価値が高いです。
走らせる
npx vitest run Test Files 2 passed (2)
Tests 5 passed (5)
Duration 1.29snpx vitest(runなし)だと見張りモードになり、保存するたびに関係するテストだけ走ります。開発中はこちらが快適です。
落ちたテストの読み方
わざと期待値を間違えると、こう出ます。
FAIL src/components/AddBirdForm.test.tsx > AddBirdForm > 入力した値がonAddに渡る
AssertionError: expected "vi.fn()" to be called with arguments: [ ObjectContaining{…} ]
Received:
1st vi.fn() call:
[
- ObjectContaining {
- "area": "うみのがけ",
+ {
+ "area": "がけ",
+ "emoji": "🐧",
"name": "ペンギン",
+ "rare": 1,
},
]読み方は-と+だけです。
-——テストが期待したもの+——実際に来たもの
上の例なら「areaがうみのがけのはずががけだった」。どちらが正しいかを考えて、テストかコードのどちらかを直します(テスト側が間違っていることも普通にあります)。
何をテストしないか
全部テストしようとすると破綻します。
| テストする | テストしない |
|---|---|
| 入力チェック・条件による出し分け | 見た目・余白・色(変わっても壊れていない) |
| 押したときに何が呼ばれるか | ライブラリ自体の動作(Routerが動くか、など) |
| バグが出て直した箇所(再発防止) | 単なるgetter・定数 |
| データの変換・計算 | 実装の内部(stateの名前など) |
公式ドキュメントも見てみる
testing-library.com外部サイトReact Testing Library | Testing LibraryReact Testing Library公式。冒頭の「The more your tests resemble the way your software is used, the more confidence they can give you」がこの回の考え方そのものです(英語)このレッスンのまとめ
- ViteのプロジェクトならVitest+Testing Libraryが標準的な組み合わせ
- 設定は
vite.configにtestを足すだけ(jsdom・setupFiles) render→screen.getByRole→expectが基本の3手- 探し方は
getByRoleが第一候補。data-testidは最後の手段 aria-labelを付けておくとテストからも狙える——アクセシビリティと同じ投資- 操作は
await userEvent.click(…)、呼ばれたかはvi.fn()で確かめる - 落ちたら
-(期待)と+(実際)だけ読む - 見た目やライブラリの動作はテストしない。ふるまいだけ書く
次は、本物のブラウザで通しで試すテストです。ここまでのテストでは届かない範囲を埋めます。