Chapter 04

同期的カウンタコンポーネントのテスト

Satoshi Takeda
Satoshi Takeda
2021.02.24に更新

カウンタコンポーネント実装

数多のサンプルでお約束のように出てくるカウンタコンポーネントに、ここでも登場してもらうことにしますが、実装詳細について深く説明するつもりはありません。カウンタの状態とアップデートを内部で持っており、ボタン押下で数字がインクリメントされる単純なコンポーネントです。

src/components/Counter/Counter.tsx
export function Counter() {
  const [count, setCount] = useState(0)
  return (
    <>
      <div>Count: {count}</div>
      <div>
        <button onClick={() => setCount(count + 1)}>Increment</button>
      </div>
    </>
  )
}

方針を思い出す

ここで内部状態がどうだとか、ボタン要素のハンドラがどうだとかの実装の詳細をテストしたいわけではなかったというのは前述でも書いたとおりです。

「Web アプリケーションがどのようにユーザーに見えるか、操作されるか」 を想定しここではテストをしていきましょう。

  1. ユーザーの画面に想定している画面(DOM)が表示される
  2. ユーザーがボタンを押下するとテキストのカウンタが増す

この 2 点をテストコードで表現します。

テストコード

短いテストコードなので以下にファイル内の全記術を記載します。

Counter.test.tsx
import { cleanup, fireEvent, render, screen } from "@testing-library/react"

import { Counter } from "./"

describe("Counter", () => {
  afterEach(() => {
    cleanup()
  })
  test("render", () => {
    const { asFragment } = render(<Counter />)
    expect(asFragment()).toMatchSnapshot()
  })
  test("click:count", () => {
    render(<Counter />)
    const button = screen.getByText("Increment")
    fireEvent.click(button)
    fireEvent.click(button)
    screen.getByText("Count: 2")
  })
})

テストファイルにはこういったフォーマットがよく見られますが大枠の意味だけまずはとらえていきます。

describe("Counter", () => {/* ... */}) では Counter というテストグループ名が指定されいます。続いて render といったテストケース、click:count といったテストケースが記述されています(実際の運用では何をしているテストケースか分かりやすいほうが本当はよいでしょうね)。

afterEach にはテストケース終了毎に React コンポーネントを画面から unmount するためのクリーンナップ用ヘルパー関数である、cleanup() が実行されています。

何をテストしているのか

Counter.test.tsx
test("render", () => {
  const { asFragment } = render(<Counter />)
  expect(asFragment()).toMatchSnapshot()
})

このテストケースでは 1 の「ユーザーの画面に想定している画面(DOM)が表示される」ことをテストしようとしています。スナップショットテストといって、最終的な関数の出力である DOM を静的なファイルとして書き出すことで次回テスト時に変更検知ができるしくみです。Jest によってスナップショットファイルは初回実行時に別途格納されます

次回以降正しい変更をしたかは開発者が都度判断しスナップショットのアップデートをしていく必要があります。誤ったスナップショットのアップデートがされた場合、スナップショットは不正なまま確認ヨシの Green を出し続けている状態ですので注意が必要です。

またスナップショットを使わず、ユーザーの画面には初期状態で Counter がゼロをである画面(DOM)が表示されている、といったことを書くことも可能です。

 test("render", () => {
-  const { asFragment } = render(<Counter />)
-  expect(asFragment()).toMatchSnapshot()
+  const { getByText } = render(<Counter />)
+  getByText("Count: 0")
 })

2 つめのテストケースについてはユーザーの操作がコードにも現れています。

test("click:count", () => {
  render(<Counter />)
  const button = screen.getByText("Increment")
  // 上記で取得したボタン要素を 2 回押下すると
  fireEvent.click(button)
  fireEvent.click(button)
  // 画面に `Count: 2` といったテキストが表示される
  screen.getByText("Count: 2")
})

手続き的かつ簡潔に記述されたテストコードは、ユーザー操作によって画面が変更したことをわかりやすく示しています。

ソフトウェアがどうユーザーによって利用されどういったことがコンポーネントの責務なのかが端的に表現され、記述がシンプルで回りくどい説明は一切ありません。

React Testing Library を使うにあたって基礎的な考え方と方針に従ったテストコードの考え方を、カウンタコンポーネントのテストコードから少し説明しました。

次章は引き続きカウンタコンポーネントを扱いますが、非同期でカウントアップされるコンポーネントになります。技術的にどうアプローチすると非同期によって状態が変化するコンポーネントをテストできるか見ていきましょう。