テスト初心者がHooksのテストを書いた振り返りとハマった点について

##はじめに
この記事は[React Advent Calendar 2019](https://qiita.com/advent-calendar/2019/react) 12日目の記事です。
はじめまして、@bebeatro:hamster:と申します！最近流行りの未経験からエンジニアというやつで今年からエンジニアになったピカピカの1年生です:boy_tone1:

元々趣味でReact/Typescriptを触っていたのですが、趣味が高じて(+前職の色々なトラブルにより…)フロントエンドエンジニアに転職しました!:v:ただ、趣味と仕事の世界はなかなか異なりまして…:sob:
特にテストは、趣味の間は全く書かず、転職して１からなぜ書くのか、どのように書くのかを実戦と共に学んでいきました。
そんな中で吸収できたことやハマったポイントについて、振り返りも兼ねて記事にまとめたいなーっと考えていたところ、ちょうどReactアドベントカレンダーに枠が１つ空いていましたので、拙筆ではありますが、書かせていただきます！:pencil:

##Hooksについて
みなさんHooks使っていますか？？おそらくReactのアドベントカレンダーを見ている人は、ほぼ100％Hooksを使用していると思いますが、おさらいということで[公式](https://ja.reactjs.org/docs/hooks-overview.html)を参照してみますと、

> フックとは、関数コンポーネントに state やライフサイクルといった React の機能を “接続する (hook into)” ための関数です。

以上のように定義されています。

そう関数なんですね！`useState`のようなHooksを用いることで、ReactのFunctional Componentはステート管理を行うことが可能となり、`useEffect`のようなHooksを用いることでFunctional Componentでありながら、APIの呼び出しなどが可能となります。

さらにHooksの真価はカスタムフックにあります。これらのHooksや他の関数などを組み合わせることで、オリジナルのHooksを作ることが可能なのです。この簡単さと様々な機能を取り込める柔軟さは本当に革命的だと思います!:dancer:
カスタムフックのメリットについては、@saitoeku3さんが前日にちょうど実例込みでわかりやすく書いてくださってるので、そちらをご覧下さい！

[なぜカスタムフックを作るのか](https://qiita.com/saitoeku3/items/35dbf4eedd544ebde89c)

ただ、そんな便利なカスタムフックですがテストとなると少々面倒です:sweat:

##カスタムフックのテストに係る問題
カスタムフックのテストにあたり1つ簡単なカスタムフックを書いてみます。

```typescript:useCounter.ts
import { useState, useCallback } from "react";

export const useCounter = () => {
  const [counter, setCount] = useState<number>(0);

  const incrementCounter = useCallback(() => setCount(counter + 1), [counter]);
  const decrementCounter = useCallback(() => setCount(counter - 1), [counter]);

  return { counter, incrementCounter, decrementCounter };
};

```
というわけで、この`useCounter`を用いることにより`counter`変数と、counterを増加させる`incrementCount`関数、また減少させる`decrementCount`関数をコンポーネントは受け取ることができます。
それでは、このカスタムフックのテストを書いてみましょう。テストはJSのデファクトスタンダードである`jest`で試してみます。

```typescript:useCounter.test.ts
import { useCounter } from './useCounter'

test('useCounter Hooks Test', () => {
  const { counter } = useCounter()

  expect(counter).toBe(0)
})

```

すると以下のようなエラーが出てきました

```typescript
 FAIL  src/advent/hooks/useCounter.test.ts
  ✕  (3ms)

  ● 

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.


```
はい、Hooksを触ったことがある人なら一度は目にしたことがある`Invalid hooks call`ですね！
考えてみれば当然なわけで、HooksはFunctional Componentの中で使用しなければならないのに、jestの中で実行すればそりゃルール違反になります。それでは、どのようにテストを行うべきでしょうか？:thinking:

##react-hooks-testing-libraryを使おう
というわけで、見出しが全てなのですが、[**react-hooks-testing-library**](https://react-hooks-testing-library.com/)を使えばまるっと解決です。react-hooks-testing-libraryは、

`npm install --save-dev @testing-library/react-hooks`
または
`yarn add -D @testing-library/react-hooks`

でちゃちゃっとインストールしちゃってください！
(なおreact-hooks-testing-libraryは、npm上では`@testing-library/react-hooks`という名前なので、以後こちらの名前を使います)
そして、`@testing-library/react-hooks`を使ったテストコードは以下のようになります。

```typescript
import { renderHook, act } from "@testing-library/react-hooks";
import { useCounter } from "./useCounter";

test("useCounterフックのテスト", () => {
  const { result } = renderHook(() => useCounter());

  expect(result.current.counter).toBe(0);

  act(() => {
    result.current.incrementCounter();
  });

  expect(result.current.counter).toBe(1);

  act(() => {
    result.current.decrementCounter();
  });

  expect(result.current.counter).toBe(0);
});

```

`renderHook`関数の中で、カスタムフックを発火させることにより、擬似的にカスタムフックをFunctional Componentの中で発火させている状態にします。
そして、`renderHook`は`result`オブジェクトを返します。
この`result`オブジェクトは`current`を有しており、これがカスタムフックの返り値と等しくなります。（今回の場合は、`result.current === {counter, incrementCounter, decrementCounter}`）
したがって、今回の`useCounter`の場合は`result.current.counter`をexpectで比較することで変数の値をテストすることが可能になります！！:tada:

また、`result.current.incrementCounter`のような関数に関しても、`@testing-library/react-hooks`から同様にimportしている`act`関数の中で発火させればFunctional Component内で発火させるのと同様の結果が期待できます！
したがって、上記のテストコードのように`incrementCounter`を一回発火させることにより`counter`の数字が1となり、`decrementCounter`が発火することにより、0に戻ります。
いかがでしょうか？？無事にカスタムフックのテストを行うことができました！:clap:


##時間の流れを伴うカスタムフックについて
ここまでは`@testing-library/react-hooks`のドキュメントを見れば、すぐに分かる内容です。ここからは、より実践的なカスタムフックについてのテストを書こうと思います！

サンプルアプリとしてFizzBuzzカラータイマーというアプリを作成しました

**GitHub Pages**
https://bebetaro.github.io/2019QiitaReactAdvent/

**GitHub**
https://github.com/bebetaro/2019QiitaReactAdvent

シンプルな秒ごとにカウントするタイマー機能にFizzBuzzを組み合わせまして、タイマーが3の倍数のときにはFizzとなり文字色が赤になり、5の倍数ではBuzzのとなり青、そして15がFizzBuzzで緑色になる。それだけです。

メインとなるコンポーネントは以下のような構成になっています。
単純にカスタムフックを呼び出し、色と現在のFizzBuzzを決めているだけですね。

```typescript:App.tsx
/** @jsx jsx */
import { jsx } from "@emotion/core";
import React from "react";
import { useChangeColor, useFizzBuzz } from "./hooks";
import { timer, root } from "./style";

const App: React.FC = () => {
  const value = useFizzBuzz();
  const color = useChangeColor(value);

  return (
    <div css={root}>
      <h1 css={timer(color)}>Fizz Buzz Color Timer</h1>
      <div>{value}</div>
    </div>
  );
};

export default App;


```

この２つのカスタムフックのコードはそれぞれ

```typescript:useFizzBuzz.ts
import { useState, useEffect } from "react";
import { FizzBuzz } from "../type";

export const useFizzBuzz = () => {
  const [counter, setCounter] = useState<number>(0);
  const [value, setValue] = useState<FizzBuzz>(0);
  
  // counterを１秒に１ずつ増やしていくuseEffect 
  useEffect(() => {
    const timeID = setInterval(() => {
      setCounter(prevCounter => prevCounter + 1);
    }, 1000);
    return () => clearInterval(timeID);
  }, []);

  //現在の数字に応じて、数字かFizzBuzzがセットされる
  useEffect(() => {
    const newValue = fizzBuzzGenerator(counter);
    setValue(newValue);
  }, [counter]);

  return value;
};

const fizzBuzzGenerator = (args: number): FizzBuzz => {
  if (args === 0) {
    return args;
  } else if (args % 15 === 0) {
    return "FizzBuzz";
  } else if (args % 5 === 0) {
    return "Buzz";
  } else if (args % 3 === 0) {
    return "Fizz";
  } else {
    return args;
  }
};

```

```typescript:useChangeColor.ts
import { useState, useEffect } from "react";
import { FizzBuzz, ColorProps } from "../type";

export const useChangeColor = (value: FizzBuzz) => {
  const [color, setColor] = useState<ColorProps>("black");
  
  //FizzBuzzと数字に応じて色を決めています
  useEffect(() => {
    if (typeof value === "number") {
      setColor("black");
    } else {
      switch (value) {
        case "Fizz":
          setColor("red");
          break;
        case "Buzz":
          setColor("blue");
          break;
        case "FizzBuzz":
          setColor("green");
          break;
        default:
          setColor("black");
      }
    }
  }, [value]);

  return color;
};

```
となっています。
`useFizzBuzz`にて数字かFizzBuzzかを表示するかを決め、その値を`useChangeColor`に渡すことで、変化する色を決めている感じです。
それでは、このカスタムフックのテストを書いていこうと思います！

##useChangeColorのテスト
まずは簡単な`useChangeColor`のテストは以下のようになります。

```typescript
import { renderHook } from "@testing-library/react-hooks";
import { useChangeColor } from "./useChangeColor";
import { FizzBuzz } from "../type";

test("useChangeColorフックのテスト", () => {

  let initialValue: FizzBuzz = 0;

  const { result, rerender } = renderHook(() => useChangeColor(initialValue));

  expect(result.current).toBe("black");

  initialValue = "FizzBuzz";
  rerender();
  expect(result.current).toBe("green");

  initialValue = "Buzz";
  rerender();
  expect(result.current).toBe("blue");

  initialValue = "Fizz";
  rerender();
  expect(result.current).toBe("red");
});

```

ポイントは`result`と一緒に渡されている`rerender`になります！
`rerender`を発火させることで、`renderHook`に渡しているカスタムフックに文字通り再レンダーがかかります。これにより、`useEffect`がdepsを再評価し、値が異なっていれば再度実行されます。テストでも`initialValue`の変化に対して期待通りの結果が得られていますね！useEffectなんかのテストにはぴったりですね！:smile:
続いて、`useFizzBuzz`のテストを行っていきます！

## useFizzBuzzのテストとハマったポイント
`useFizzBuzz`は1秒毎に数字を１ずつカウントしていく`setInterval`の`useEffect`とそのカウントに応じてFizzBuzzを返す`useEffect`の２つの機能があります。それでは、`@testing-library/react-hooks`でこのような時間の制御はどのように行うのでしょうか？直感的には以下のようなテストコードが考えれます。

```typescript:useFizzBuzz.test.ts
import { renderHook } from "@testing-library/react-hooks";
import { useFizzBuzz } from "./useFizzBuzz";

test("useFizzBuzzフックのテスト失敗例１", () => {
  const { result } = renderHook(() => useFizzBuzz());

  expect(result.current).toBe(0);

  setTimeout(() => {
    expect(result.current).toBe("Fizz");
  }, 3000);
});

```
`setTimeout`で3秒後にexpectで評価を行えば、カウントが3だからFizzが返ってくるはずというテストですね。
実はこのテストは一見パスします！「お？いけるのか？:astonished:」と当初は考えたのですが、よくよく見てみると実は`setTimeout`内を実行せずに終わっているんですね。したがって、評価する値がBuzzでもパスします。これはいけない!:no_good:

ここでハマってしまい`setTimeout`をjestではどう処理しているのかグーグル先生に聞いてみたところ、jest公式ページに[ぴったりなTimer Mock](https://jestjs.io/docs/ja/timer-mocks#%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%9F%E6%99%82%E9%96%93%E3%81%A7%E3%82%BF%E3%82%A4%E3%83%9E%E3%83%BC%E3%82%92%E9%80%B2%E3%82%81%E3%82%8B)についての記述がありました！

そこでjestの公式サンプルに沿って改善してみました！

```typescript
import { renderHook } from "@testing-library/react-hooks";
import { useFizzBuzz } from "./useFizzBuzz";

test("useFizzBuzzフックのテスト失敗例２", () => {
  const { result } = renderHook(() => useFizzBuzz());
  expect(result.current).toBe(0);

  // jest.advanceTimersByTimerが指定時間分jestの中の時間を進めます
  jest.advanceTimersByTime(3000);

  expect(result.current).toBe("Fizz");
});


```
これもパスしました！しかも、`expect(result.current).toBe("Fizz")`の評価する値をBuzzや3に変化させるとテストが失敗します！ちゃんと評価してくれているということですね！:thumbsup:
しかし、実はこの方法ではパスしているものの、jestがWarningを吐いてしまいました。
Warningの中身を見てみると... :eyes: 

```typescript
    Warning: An update to TestHook inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
        in TestHook
        in Suspense

```

そうなんです！今回のような時間経過であれカスタムフックのstateをアップデートさせるものは全て`act`の中で行う必要があるんですね！これが最後のピースです。この`act`を反映させたテストが以下になります。

```typescript
import { renderHook, act } from "@testing-library/react-hooks";
import { useFizzBuzz } from "./useFizzBuzz";

test("useFizzBuzzフックの成功例", async () => {
  const { result } = renderHook(() => useFizzBuzz());

  expect(result.current).toBe(0);

  //３秒経過　合計３なのでFizzになるはず
  act(() => {
    jest.advanceTimersByTime(3000);
  });

  expect(result.current).toBe("Fizz");

  //2秒経過　合計5なのでBuzzになるはず
  act(() => {
    jest.advanceTimersByTime(2000);
  });
  expect(result.current).toBe("Buzz");

  //10秒経過　合計15なのでFizzBuzzになるはず
  act(() => {
    jest.advanceTimersByTime(10000);
  });
  expect(result.current).toBe("FizzBuzz");

  //1秒経過　合計16なので16になるはず
  act(() => {
    jest.advanceTimersByTime(1000);
  });
  expect(result.current).toBe(16);
});

```

これでテストもパスしてWarningも発生しません:hugging:
無事にコンポーネントのロジック部分をHooksに逃し、コンポーネントの可読性とシンプルさを保ちつつ、テストでロジックの品質面も保証することができました！やったね！たえちゃん！:bear:

##まとめ
ということで、Hooksのテストについて書かせていただきました。
私がテスト自体に不慣れなことと、Hooksのテストについて日本語ドキュメントが少ないことから、今回のような時間経過のケースでハマってしまい解消するのに結構時間がかかってしまいました…。
この記事が私のようにHooksのテストで悩んでいる方の一助になれば幸いです:bow:

Hooksめちゃくちゃ便利ですし、コミュニティ的にも活気がありまくるので未来しか感じてないです。また、今回紹介した`@testing-library/react-hooks`はライブラリのアップデートも盛んに行われていますし、今回は紹介できなかったのですが、async/awaitやContextのテストも簡単に書けます！
みなさんもどんどんカスタムフック書いて、どんどんテストを書いて、Hooksを盛り上げていきましょう！:confetti_ball:

最後に、私自身、まだまだペーペーエンジニアですので文章中の誤りなどありましたら、本記事か[Twitter](https://twitter.com/bebetaro1)でご指摘いただけると助かります！ 
明日は@fsubalさんが「useSelectorを用いたリファクタリング」について書いてくれます。昨日、本日、明日とHooksが続いているあたり、やはりHooks熱いぜ…！:fire:

##おまけ
`@testing-library/react-hooks`のドキュメントを見ると分かるのですが、実は[今回のユースケースにぴったりのAPIは既にあります。](https://react-hooks-testing-library.com/reference/api#async-utilities)しかし、実際にそのAPIを呼び出そうとするとTypescriptの場合、型定義がまだ無く呼び出すことができません。自分で型定義を作ればその限りではないと思いますが、今回のようなユースケースであればTimer Mocksのほうが早くお手軽に実行できるかなとは思います。
また、そのうち型定義がアップデートされれば本記事も不用になるかもですが、そのときはそのときでアップデートされた方法についてQiitaを書ければと思います！:muscle: