React Hooksのテストを書いてみよう
Meguro.es #22
08/08
自己紹介
- 名前: markey, markey_koichan(Twitter)
- 職業: フロントエンドエンジニア
- スキル: React/Vue/TypeScript
- 趣味: 日本語ラップ、MCバトル鑑賞
人生初LT、お手柔らかに🙏
テスト、書いてますか?
React Hooksのテスト、書いてますか?
React Hooks誕生後の世界
- UIコンポーネント→Function Component
- ビジネスロジック→Custom Hook
こんないいことが
- UIとロジックが分離され、テスタブルなコードに
- ビジネスロジックをCustom Hookに集約する(APIからのデータ取得・加工、状態、ハンドラー)
- UIコンポーネントの開発は受け取るPropsの定義と、それらを元にした表示のみ
- UIコンポーネント開発者はロジック部分やAPIの仕様などを一切知る必要がない
- 複数の開発者が同一ページのUIとロジックを並行開発することが可能
今こそ、テストをやるチャンス🎉
それぞれのテスト
- UIコンポーネント→Storybook + reg-suit
- ビジネスロジック→@testing-library/react-hooks
今日話すのはここ
- UIコンポーネント→Storybook + reg-suit
- ビジネスロジック→@testing-library/react-hooks
@testing-library/react-hooks
パッケージの追加
yarn add --dev @testing-library/react-hooks
# もしくは
npm install --save-dev @testing-library/react-hooks
@testing-library/react-hooksを使ったシンプルなテスト
import { useState, useCallback } from 'React'
export default function useCounter() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount((x) => x + 1), [])
return { count, increment }
}
import { renderHook } from '@testing-library/react-hooks'
import useCounter from '.'
test('should use counter', () => {
// hookを実行
const { result } = renderHook(() => useCounter())
// resut.currentからhookの返り値を取得
expect(result.current.count).toBe(0)
expect(typeof result.current.increment).toBe('function')
})
ブラウザ上でのhookの動作をシミュレートして、
値を更新するには?🤔
test('should increment counter', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count).toBe(0)
// actでラップする
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(1)
})
Contextから値を取得するときはどうする?🤔
(Redux, Apollo Client, ...)
const CounterStepContext = React.createContext(1)
export const CounterStepProvider = ({ step, children }) => (
<CounterStepContext.Provider value={step}>{children}</CounterStepContext.Provider>
)
export function useCounter() {
const [count, setCount] = useState(0)
// incrementする値はContextから取得
const step = useContext(CounterStepContext)
const increment = useCallback(() => setCount((x) => x + step), [step])
return { count, increment }
}
import { useCounter, CounterStepProvider } from '.'
test('should use custom step when incrementing', () => {
const wrapper = ({ children }) => (
<CounterStepProvider step={2}>{children}</CounterStepProvider>
)
// renderHookの第2引数にwrapperオプションを指定
const { result } = renderHook(() => useCounter(), { wrapper })
expect(result.current.count).toBe(0)
act(() => {
result.current.increment()
})
expect(result.current.count).toBe(2)
})
非同期な関数の実行はどうする?🤔
export default function useCounter() {
const [count, setCount] = useState(0)
const increment = useCallback(() => setCount((x) => x + 1), [])
// 0.1秒後に非同期にincrementする
const incrementAsync = useCallback(() => setTimeout(increment, 100), [increment])
return { count, increment, incrementAsync }
}
test('should increment counter after delay', async () => {
const { result, waitForNextUpdate } = renderHook(() => useCounter())
result.current.incrementAsync()
// 実行直後はまだincrementされていない
expect(result.current.count).toBe(0)
// Promiseがresolveされるまで待つ
await waitForNextUpdate()
expect(result.current.count).toBe(1)
})
@testing-library/react-hooksで
快適なテストライフを👍
ご清聴ありがとうございました!
(副業募集中...ReactとTypeScriptチョットカケマス...)