Reactでカスタムフックを作った時のテストはどうするの？

## はじめに

Reactの開発に関わっていると、親コンポーネントからuseStateやfunctionなどPropsで子コンポーネントたちにばら撒く構成でロジック部分が数百行に肥大化…。

「Viewは結局何ナリか？」

と、トラックパッドが削れすり減るようなスクロールを行う日々を過ごしている人もいるんじゃないでしょうか？

Viewとロジックを切り分ける際の常套手段？であるカスタムフックを作るとしたらどう記述するのか？
その時テストの責任区分はどうすればテストカバレッジを高く取れるのか？をまとめてみたくて記事にします。

## カスタムフックの書き方

すでにご存知の方はぶっ飛ばしていただいてもOKです。


元々の実装ファイルが下記のような物だったとします。
buttonのクリックでモーダルもどきが出たり消えたりするだけの子です。

```tsx
import React from 'react'

const ModalComponent = () => {
  const [isShown, setIsShown] = useState(false)

  const toggleModal = () => {
    setIsShown(!isShown)
  }
  return (
    <div>
      <button onClick={toggleModal}>Toggle Modal</button>
      {isShown && (
        <div>
          <p>Modal Content</p>
          <button onClick={toggleModal}>Close Modal</button>
        </div>
      )}
    </div>
  )
}

export default ModalComponent
```

このファイルの
```tsx
  const [isShown, setIsShown] = useState(false)

  const toggleModal = () => {
    setIsShown(!isShown)
  }
```
この部分がロジックの部分です。

皆さんの実装コードはこの部分に大量のuseStateやfunctionが長蛇の列をなしていて、
いくらスクロールをしてもreturn () のところに到達しない…という状況ではないでしょうか？

カスタムフックはこのロジック部分をfunctionとして切り出します。

```tsx
import { useState } from 'react'

export const useModal = () => {
  const [isShown, setIsShown] = useState(false)

  const toggleModal = () => {
    setIsShown(!isShown)
  }

  return {
    isShown,
    toggleModal,
  }
}
```

View側で使う変数や関数をreturn {} の形で返すところがポイントになります。


実装側では、
```tsx
import React from 'react'
import { useModal } from './useModal'

const ModalComponent = () => {
  const { isShown, toggleModal } = useModal()

  return (
    <div>
      <button onClick={toggleModal}>Toggle Modal</button>
      {isShown && (
        <div>
          <p>Modal Content</p>
          <button onClick={toggleModal}>Close Modal</button>
        </div>
      )}
    </div>
  )
}

export default ModalComponent
```

切り出したファイルを `import { useModal } from './useModal'` としてインポートし、
`const { isShown, toggleModal } = useModal()` このようにして呼び出します。

サンプルのコードは、ロジック部分の行数が少なかったのであまり違いが見て取れませんが、
関数が増えてきた場合は大分コンパクトに収まることが想像できるのではないでしょうか？

## テストの責任区分はどうなる？

サクッとまとめると、以下のような切り口でテストをするのが良さそうです。

- カスタムフックのテスト
    - 状態管理と関数の動作をテスト
    - 初期状態、状態変化、関数の引数、関数の呼び出し結果をテスト
- Viewコンポーネントのテスト
    - コンポーネントのレンダリングとユーザーの振る舞いをテスト
    - ボタンクリックに対するモーダルの表示・非表示の動作をテスト

## カスタムフックのテスト

renderHookを使用してカスタムフックをレンダリングしてテストを作成します。

```tsx
import { renderHook, act } from '@testing-library/react-hooks'
import { useModal } from './useModal'

test('should toggle modal visibility', () => {
  const { result } = renderHook(() => useModal())

  // 初期状態の確認
  expect(result.current.isShown).toBe(false)

  // モーダルの表示をトグル
  act(() => {
    result.current.toggleModal()
  })
  expect(result.current.isShown).toBe(true)

  // モーダルの非表示をトグル
  act(() => {
    result.current.toggleModal()
  })
  expect(result.current.isShown).toBe(false)
})
```

stateやメソッドを直接的に呼び出して、
実際の値がどのようになっているか？をテストしたり、
この例ではtoggleModal関数を呼び出してisShownが切り替わることをテストできます。

## Viewコンポーネントのテスト

コンポーネントをrenderして各種要素をクリックするなどして、
モーダルもどきが表示されていることを確認するようなテストを記述します。

```tsx
import React from 'react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ModalComponent from './ModalComponent'

test('should open and close the modal', async () => {
  render(<ModalComponent />)
  const user = userEvent.setup()

  // トグルボタンを取得
  const toggleButton = screen.getByText('Toggle Modal')

  // モーダルを表示
  await user.click(toggleButton)
  expect(screen.getByText('Modal Content')).toBeInTheDocument()

  // クローズボタンを取得
  const closeButton = screen.getByText('Close Modal')

  // モーダルを閉じる
  await user.click(closeButton)
  expect(screen.queryByText('Modal Content')).not.toBeInTheDocument()
})
```

userEventはuserEvent.setup()でインスタンス化してから使うことが推奨されているようです。
https://testing-library.com/docs/user-event/intro/

## まとめ

userEventの使い方も@testing-library/user-eventのv13.0.0から.setup()を使ってインスタンス化して使うことが推奨されている事を今更ながらにしれたのもちょっとした学びポイントでした。

Viewでは見えるところや押せるところに対するテストを行い、カスタムフックのロジック部分では、それぞれの関数が正しく機能しているかをrenderHookを使うことでテストできることがわかりました。

カスタムフックの部分は別ファイルに切り出した方がコードは見やすいのか？は議論になりそうですが、簡単な例とともに記述方法をまとめられたので勉強になりました。

それではみなさま良いコードライフを。