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


  • 公式ドキュメント
  • React Hooksのテストユーティリティライブラリ
  • hookをシンプルな関数のように実行でき(ラッパーコンポーネントは不要)、
    hookの更新も容易
  • メインのファイルはわずか93行

パッケージの追加


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チョットカケマス...)