hooks含め、ロジックのテストはできるようになったぞ...!
ただ、それではComponentが正しく動いているかまでは担保ができないぞ...
そんな時に行うのが、「Componentテスト」です。
Componentをテストする方法としては、通常の単体テストと、スナップショットテストがあります。
準備
reactのComponentテストを行うには、@testing-library/reactをインストールします。
yarn add -D @testing-library/react
公式が提供しているreact-test-rendererを使っているサイトも多くみられますが、
@testing-library/reactでも同様な使い方ができます。
また、公式も@testing-library/reactを推奨しています。
テストするComponent
import React, { VFC, useState } from "react";
type Props = {
  max: number;
};
export const Pagination: VFC<Props> = ({ max }) => {
  // setCurrentIndex、pageIndexArray あたりは本来はカスタムhooks化 & 別途ユニットテストすべき。
  // そのため、今回はテスト対象外
  const [currentIndex, setCurrentIndex] = useState(1);
  // 1〜max値までの数字の配列を作る
  const pageIndexArray = [...Array(max)].map((_, index) => index + 1);
  return (
    <div>
      <ul>
        {pageIndexArray.map((index) => (
          <li key={`paginationItem_${index}`}>
            {index} {index === currentIndex && <span>◀︎ now</span>}
          </li>
        ))}
      </ul>
      <div>
        <button
          onClick={() => setCurrentIndex((prev) => prev - 1)}
          disabled={currentIndex <= 1}
        >
          prev
        </button>
        <button
          onClick={() => setCurrentIndex((prev) => prev + 1)}
          disabled={currentIndex >= max}
        >
          next
        </button>
      </div>
    </div>
  );
};
コンポーネントの単体テスト
どんな時に必要?
主に、コンポーネントが正しく動くかを検証する時に使います。
書き方
コンポーネントの要件としては、下記が挙げられます。
- 
currentIndexとindexが一致した時に、「◀︎ now」が表示される - 
currentIndexが1以下の時、prevボタンがdisabledになる - 
currentIndexがmax以上の時、nextボタンがdisabledになる 
Componentのレンダリング
まずは、テストしたいComponentをレンダリングします。
レンダリング後、screenを用いることでComponentにアクセスできるようになります。
import { render, screen } from '@testing-library/react';
describe('Pagination', () => {
  test('currentIndexとindexが一致した時に、"◀︎ now"が表示される', () => {
    // Componentをレンダリング
    render(<Pagination max={5} />);
    // Componentにアクセス
    screen.debug();
  }
}
要素の検索
要素を検索するには、基本的にはqueryByを用いて検索します。
(他にも複数検索するgetAllByや、非同期の時に用いるfindByなどもあります。)
- queryByText - 要素が持つテキスト
 - queryByRole - 要素が持つロール
- domが持つデフォルトのRole - ARIA in HTML
 - role属性 - 
role="button"など 
 - queryByLabelText - 要素が持つラベル
- aria-label
 - aria-labelledby
 - selectorも一緒に検索できる 
screen.getByLabelText('Username', {selector: 'input'}) 
 - queryByPlaceholderText
- placeholder属性
 
 - queryByAltText
- alt属性
 
 - queryByDisplayValue
- inputなどで、表示されているvalue
 
 
こんな感じ
test('currentIndexとindexが一致した時に、"◀︎ now"が表示される', () => {
  render(<Pagination max={5} />);
  screen.queryByText(/1/)
});
アサーション
あとは、いつも通り検証するだけです。
jestのマッチャーに加えて、jset-domに含まれているマッチャーが使えます。
expect(screen.queryByText(/1/)).toHaveTextContent(/◀︎ now/);
expect(screen.queryByText(/2/)).not.toHaveTextContent(/◀︎ now/);
イベントを発火
fireEventを用いて、ボタンをclickしたりフォームをchangeさせたりすることができます。
import { render, screen, fireEvent } from '@testing-library/react';
describe('Pagination', () => {
  test('currentIndexとindexが一致した時に、"◀︎ now"が表示される', () => {
    // 対象Componentのレンダリング
    render(<Pagination max={5} />);
    // ユーザーの操作
    fireEvent.click(screen.queryByText(/next/));
  });
});
完成!
だいっぶ愚直に書いてしまいましたが、こんな感じになりました!
import { render, screen, fireEvent } from '@testing-library/react';
import { Pagination } from './Pagination';
describe('Pagination', () => {
  test('currentIndexとindexが一致した時に、"◀︎ now"が表示される', () => {
    // 対象Componentのレンダリング
    render(<Pagination max={5} />);
    // 要素の検索と表示の検証
    expect(screen.queryByText(/1/)).toHaveTextContent(/◀︎ now/);
    expect(screen.queryByText(/2/)).not.toHaveTextContent(/◀︎ now/);
    // ユーザーの操作
    fireEvent.click(screen.queryByText(/next/));
    expect(screen.queryByText(/2/)).toHaveTextContent(/◀︎ now/);
    expect(screen.queryByText(/1/)).not.toHaveTextContent(/◀︎ now/);
  });
  test('currentIndexが1以下の時、prevボタンがdisabledになる', () => {
    render(<Pagination max={5} />);
    expect(screen.queryByText(/prev/)).toBeDisabled();
    fireEvent.click(screen.queryByText(/next/));
    expect(screen.queryByText(/prev/)).toBeEnabled();
    fireEvent.click(screen.queryByText(/prev/));
    expect(screen.queryByText(/prev/)).toBeDisabled();
  });
  test('currentIndexがmax以上の時、nextボタンがdisabledになる', () => {
    render(<Pagination max={5} />);
    fireEvent.click(screen.queryByText(/next/));
    fireEvent.click(screen.queryByText(/next/));
    fireEvent.click(screen.queryByText(/next/));
    fireEvent.click(screen.queryByText(/next/));
    expect(screen.queryByText(/next/)).toBeDisabled();
  });
});
スナップショットテスト
そもそもスナップショットテストとは?
スナップショットテストは、予期していない見た目の変更があった時に検知するためのテストで、リグレッションテストの一つです。
テスト実行時に、スナップショットがなければ作成し、スナップショットがあればの一致しているかを検証します。
どんな時に必要?
主に、コンポーネントの見た目の変更を検知させたい時に使います。
書き方
test('snapshot', () => {
  const { asFragment } = render(<Pagination max={5} />);
  expect(asFragment()).toMatchSnapshot();
});
補足
Storybookのアドオンでもスナップショットテストはできるので、Storybook導入しているプロダクトではStorybookに任せちゃった方がいいかもですね!
参考
