18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[React]カスタムフックのテストを書く準備と基本的な書き方

Last updated at Posted at 2021-11-29

はじめに

カスタムフックをテストしたい!、、、となった時のライブラリの導入手順や簡単な例をまとめました。

対象読者

Reactのカスタムフックをどのようにテストするか、導入部を知りたい人

カスタムフックのテストを書く準備

以下のライブラリのインストールがまだの方は以下をインストールしてください。

npm install --save-dev @testing-library/react-hooks
npm install --save-dev react-test-renderer

ライブラリの公式ドキュメントはこちら

ライブラリの主要なAPI

ドキュメントはすぐに目を通せる分量ですが、hooksのテストを導入して、よく利用するであろうものは以下になるかなと思います。

renderHook

hooksは、関数コンポーネントの中でしか呼ぶことができないのですが、この関数を使用するとテストファイルの中でも呼ぶことができるようになります。

result

これは、result.current.**でカスタムフックの現在の戻り値を取得することが可能です。
expectでstateの検証をしたり、カスタムフックの関数を呼ぶときに使用します。

const { result } = renderHook(useCounter);
expect(result.current.count).toBe(0); // 初期値を検証

何らかのエラーが投げられた場合、result.errorが返ってきます。

11以上加算されるとエラーが投げられる場合
test("countがMAX_COUNTを超えた場合、例外がスローされること", () => {
  const { result } = renderHook(useCounter);
  expect(result.error).toBeUndefined();
  act(() => result.current.incrementByAmountValue(20));
  expect(result.error.message).toBe("10までしか加算できません");
});

rerender

useEffect等、コンポーネントが再レンダーされることをシュミレートしたい時に使われるケースが多いかなと思います。
以下のように引数に新たなpropsを渡すことが可能です。

const { result, rerender } = renderHook(
  ({ initialCount }) => useCounter(initialCount),
  {
    initialProps: { initialCount: 0 },
  }
);

rerender({ initialCount: 11 })

waitForNextUpdate

Promiseを返す関数で、stateの更新を待ちます。
設定されたtimeout値まで待機することができます。(デフォルトは1000ms)
例えば、以下のように非同期の関数を読んだ時に、2000ms待機した後に、値を評価することができます。

act(() => result.current.asyncIncrement());
await waitForNextUpdate({ timeout: 2000 });
expect(result.current.count).toBe(1);

waitFor

この関数は引数に真偽値を返す関数を渡して、それがtrueになるまで、待ちます。
何らかのアクションを起こし、値が期待した結果かどうかを判定するなどのケースで使えそうです。
intervaltimeoutがそれぞれ設定でき、デフォルトはそれぞれ50ms(interval) 1000ms(timeout)です。

他にも、useEffectのクリーンアップ関数の実行をテストしたい時に使用するunmount等が用意されています。

act

stateの更新関数を呼ぶ際は、actを利用します。

act(() => result.current.increment());

実際に書いてみる

よくあるカウンターコンポーネントでhooksをどのようにテストするか確認していきましょう。

CustomCounter
import React from "react";
import { useCounter } from "./hooks/useCustomCounter";

export const CustomCounter: React.FC = () => {
  const {
    count,
    amountVal,
    increment,
    asyncIncrement,
    incrementByAmountValue,
    onChangeAmountValue,
  } = useCounter();

  return (
    <>
      <h2>CustomHookTestSample</h2>
      <p>{count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={asyncIncrement}>+1(非同期)</button>
      <div>
        <label htmlFor="amountValue">AmountValue</label>
        <input onChange={onChangeAmountValue} id="amountValue" type="text" />
        <button onClick={() => incrementByAmountValue(amountVal)}>
          incrementByAmountValue
        </button>
      </div>
    </>
  );
};
useCustomCounter.ts
import { useCallback, useState } from "react";

export type UseCustomCounterReturnType = {
  count: number;
  amountVal: number;
  increment: () => void;
  asyncIncrement: () => void;
  incrementByAmountValue: (num: number) => void;
  onChangeAmountValue: (e: React.ChangeEvent<HTMLInputElement>) => void;
};

const MAX_COUNT = 10;

export const useCounter = (): UseCustomCounterReturnType => {
  const [count, setCount] = useState(0);
  const [amountVal, setAmountVal] = useState<number>(0);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  const sleep = (msec: number) =>
    new Promise((resolve) => setTimeout(resolve, msec));

  const asyncIncrement = useCallback(async () => {
    await sleep(1000);
    setCount(count + 1);
  }, [count]);

  const incrementByAmountValue = (amountVal: number) => {
    setCount((prevCount) => prevCount + amountVal);
  };

  const onChangeAmountValue = (e: React.ChangeEvent<HTMLInputElement>) => {
    setAmountVal(Number(e.target.value));
  };

  if (count > MAX_COUNT) {
    throw new Error("10までしか加算できません");
  }

  return {
    count,
    amountVal,
    increment,
    asyncIncrement,
    incrementByAmountValue,
    onChangeAmountValue,
  };
};

CustomCounter.test.js
import { act, renderHook } from "@testing-library/react-hooks";
import { useCounter } from "pages/TestSample/components/CustomCounter/hooks/useCustomCounter";

describe("useCustomCounter", () => {
  test("incrementボタン押下時、1加算されること", () => {
    const { result } = renderHook(useCounter);
    expect(result.current.count).toBe(0); // 初期値を検証
    act(() => result.current.increment());
    expect(result.current.count).toBe(1);
  });
  test("incrementByAmountボタン押下時に入力値が加算されること", () => {
    const { result } = renderHook(useCounter);
    act(() => result.current.incrementByAmountValue(10));
    expect(result.current.count).toBe(10);
  });
  test("countがMAX_COUNTを超えた場合、例外がスローされること", () => {
    const { result } = renderHook(useCounter);
    expect(result.error).toBeUndefined();
    act(() => result.current.incrementByAmountValue(20));
    expect(result.error.message).toBe("10までしか加算できません");
  });
  test("非同期のincrementボタン押下時、1加算されること", async () => {
    const { result, waitForNextUpdate } = renderHook(useCounter);
    expect(result.current.count).toBe(0); // 初期値を検証
    result.current.asyncIncrement();

    await waitForNextUpdate({ timeout: 3000 });

    expect(result.current.count).toBe(1);
  });
});

describe("useCustomCounter FetchUser", () => {
  test("incrementボタン押下時、1加算されること", () => {
    const { result } = renderHook(useCounter);
    expect(result.current.count).toBe(0); // 初期値を検証
    act(() => result.current.increment());
    expect(result.current.count).toBe(1);
  });
});

以上。
Reduxやfetch関数が絡むとmockが必要になりますが、そこら辺も整理してまとめたい。

18
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?