Chapter 06

カスタム Hooks のテスト、テストにおけるスパイ

Satoshi Takeda
Satoshi Takeda
2021.02.24に更新

打って変わってカスタム Hooks

Hooks が登場してからというもの、ライフサイクルに合わせた副作用をカスタム Hooks に退避させることが多くなりました。コンポーネントが扱いたい値やハンドラを返すような Hooks を作成したり、コンポーネントのライフサイクル(deps に指定する値の変更タイミング)に合わせて UI とは別の処理を切り出したりと活用するユースケースはさまざまです。

今回取り扱うカスタム Hooks はごく単純なものです。解析系 SaaS から提供される関数を使って、画面表示時にユーザー情報をメタ情報としてセットしページビューイベントを送信するといったものです。toC であれ業務系 toB であれよく求められる所作でしょう。

一度きり実行するカスタム Hooks

useInitialAnalytics.ts
export function useInitialAnalytics(user: User) {
  useEffect(() => {
    setStatus(user)
    sendPageview()
  }, [])
}

このカスタム Hooks は役割が明確です。コンポーネント内で呼び出されるとマウントした初回のみ setStatususer の引数で処理を実行し、sendPageview を実行します。

setStatussendPageview の内部実装はサードパーティが提供する関数をラップしているだけなので呼び出されたかをテストすることが重要そうです。方針はユーザーがソフトウェアを扱うようにテストをすることでしたが、ここでは少し方針から逸れて手法を変えます。

  • ユーザーが画面を表示した際に
    1. setStatususer の引数で処理が実行される
    2. sendPageview の処理が実行される

以上をテストできるユニットテストを考えてみます。

カスタム Hooks のテストとスパイ

短いのでテストをすべて掲載しましょう。ここまで単純であればテストが必ず必要かと問われると怪しいものですが。

useInitialAnalytics.test.ts
import { renderHook } from "@testing-library/react-hooks"

import { useInitialAnalytics } from "~/hooks/useInitialAnalytics"
import * as analyticsModule from "~/libs/analytics"

const spiedSetStatus = jest.spyOn(analyticsModule, "setStatus")
const spiedSendPageview = jest.spyOn(analyticsModule, "sendPageview")

test("useInitialAnalytics 初回実行", () => {
  // A.Hooks を呼び出すためのヘルパー関数
  renderHook(() => useInitialAnalytics({ id: "foo", role: "bar" }))
  expect(spiedSetStatus).toBeCalledWith({ id: "foo", role: "bar" })
  expect(spiedSendPageview).toBeCalled()
})

A では renderHook(() => /** カスタム Hooks 実行 */) のような特徴的な記述があります。React Hooks Testing Library が提供するヘルパーで Hooks をテストするために必要なものとなります。コンポーネントの render ヘルパーとよく似ています。

特徴的な部分は下記のように Jest が提供する API で特定の変数にバインドしている部分です。

const spiedSetStatus = jest.spyOn(analyticsModule, "setStatus")
const spiedSendPageview = jest.spyOn(analyticsModule, "sendPageview")

これはファイル冒頭で analyticsModule といった名前空間を定義しインポートしたモジュールが提供する setStatus という関数に対してスパイをすることを明示しています。jest.spyOn でスパイすると、実際の関数を監視してどういった引数で呼ばれたか何回よばれたかをなどを格納するインスタンスを返します。このインスタンスに対して呼び出し有無や引数が適切かをテストするといったアプローチをとっていきます。

Hooks が実行された後の関数がどう呼び出されたかのテストは下記のとおりです。

// setStatus は { id: "foo", role: "bar" } といったオブジェクトを引数にして呼び出された
expect(spiedSetStatus).toBeCalledWith({ id: "foo", role: "bar" })
// sendPageview は呼び出された
expect(spiedSendPageview).toBeCalled()

ここではカスタム Hooks によってどういったことが起きるか = 関数がどう呼び出されるかをテストしました。方針として挙げている「Web アプリケーションがどのようにユーザーに見えるか・操作されるかをトレースできるテスト」といった類のものではありませんが、カスタム Hooks によって UI のバックグラウンドで実行される処理も不安があればテストを配置したいものです。


次章と次々章では API との非同期リクエストを扱ったコンポーネントのテストについて解説していきます。window.fetch をモックしてテストする方法と、リクエストをインターセプトしレスポンスを置き換える方法を続けて提示します。比較し相互の利点も挙げていくので自分にあったものを選択すると良いでしょう。