React アプリで何をテストするのか

:::note info
**ポイント**
1. Reactアプリのテストは、セキュアで高性能、そしてユーザーフレンドリーなアプリケーションを提供するためには欠かせません
1. Reactアプリは、異なるUIコンポーネントを使用して作成されます。そのため、各コンポーネントを個別にテストし、統合したときの動作もテストすることが必要です
1. Reactアプリに対しては、要件に応じて、ユニットテスト、統合テスト、エンドツーエンドテスト、スナップショットテストを実施することが不可欠です
1. React Testing Library、Jest、Mocha、Enzyme、Cypress、Playwright、Selenium、JMeter、Jasmin、TestRailは、Reactアプリをテストするために使用される主要なツールやライブラリの一部です
:::

Reactアプリのテストは簡単な作業ではありませんが、いくつかのフレームワークやライブラリが存在します。この記事では、そのようなライブラリについて説明します。

## Webアプリをテストする必要があるのはなぜなのか

アプリケーションをテストする主な目的は、アプリがエラーなく、意図した通りに動作することを確認することです。しかし、アプリの中には開発者が常に注意を払わなければならない特定の機能があり、これらを怠るとコストがかかるエラーの繰り返しにつながる可能性があります。テストが特に重要な領域をいくつか紹介します。

- **セキュリティ**: アプリがユーザーの情報を扱う場合、セキュリティのテストは非常に重要です。データの漏洩や不正アクセスを防ぎ、ユーザーの信頼を保つために、詳細なセキュリティチェックが必要です
- **ユーザーインターフェース**: アプリの外見と感触が、ユーザーがアプリを使いたいと思うかどうかを大きく左右します。ユーザーインターフェースのテストは、アプリが視覚的に魅力的で、簡単にナビゲートできることを確認することが目的です
- **パフォーマンス**: アプリが遅かったり、リソースを過剰に消費したりすると、ユーザーの満足度が著しく低下します。パフォーマンステストにより、アプリが快適な速度で正常に動作することを確認します
- **互換性**: 異なるデバイス、オペレーティングシステム、ブラウザでアプリが正しく動作するかを検証します。互換性テストにより、幅広いユーザーが問題なくアプリを使用できるようにします
- **エラーハンドリング**: アプリが予期せぬ入力や状況に遭遇した際に、適切に対処できるかどうかをテストします。これにより、エラーが発生してもアプリがクラッシュしたりフリーズしたりしないことを確認します
- **機能テスト**: アプリの各機能が、設計どおりに正確に動作することを保証します。これにより、ユーザーが期待する通りの結果が得られることが確認されます

## Reactアプリで何をテストするのか

Reactアプリケーションのテストにおいて、何をテストすべきかについて悩むのは普通のことです。アプリが比較的シンプルなデータを扱っていても、時には複雑な動作をする場合があります。テストを行う上での優先順位を決めることは、開発プロセスの重要な部分です。テストプロセスを始める際に最適なポイントをいくつかご紹介します。

- **コンポーネントのレンダリング**: Reactアプリでは、さまざまな状況下でコンポーネントが正しくレンダリングされるかを確認することが重要です。特に、ユーザーのアクションやデータの変化に応じて、UIが予想通りに更新されるかをテストします
- **ユーザーインタラクション**: ボタンクリックやフォームの送信など、ユーザーがアプリとどのようにやり取りするかをシミュレートするテストが重要です。これにより、ユーザーのアクションが期待通りの結果をもたらすことを確認します
- **状態管理のテスト**: Reactアプリでは、状態(state)の管理が中心となります。適切な状態変更やプロップの受け渡しが行われているかをテストすることが重要です
- **APIとの連携**: 外部APIからデータを取得したり、データを送信したりする機能は、アプリの重要な部分です。API呼び出しに関連するコードが正確に動作することを確認するテストを行います
- **統合テスト**: 個々のコンポーネントや機能が正しく動作することを確認した後は、それらが互いにうまく連携して全体として期待通りの動作をするかをテストします。ここでは、アプリの主要なユーザーフローを通してテストを行います
- **エッジケースとエラーハンドリング**: 通常の動作だけでなく、エッジケースやエラー状況でもアプリが適切に動作するかをテストします。例えば、無効な入力値やネットワークエラーが発生した場合の挙動などです

## 必要なライブラリとツール

Reactのテストライブラリやフレームワークは、ユーザーに最高のアプリケーションを提供するために必要不可欠です。しかし、これらのフレームワークにはそれぞれに特色があります。ここでは、Reactアプリケーションのテスト用のいくつかのReactテストライブラリやツールについて確認していきましょう。

### Jest

https://github.com/jestjs/jest

Jestは、Reactコミュニティによって推奨されている人気のあるReactテストフレームワークまたはテストランナーです。テストチームは、大規模な企業のアプリケーションをテストするためにこのツールを好んで使用します。Airbnb、Uber、Facebookといった企業はすでにこのツールを使用しています。

**Jestのいくつかの利点**

- 大規模なテストケースを追跡できる
- 設定や使用が簡単
- Jestでのスナップショットの取得
- API関数のモックが可能
- 並列化テスト方法を実施する能力

### Mocha

https://github.com/mochajs/mocha

MochaはNode.js上で動作し、テスターはReactを使用して開発されたアプリケーションをチェックするためにそれを使用します。これは開発者が非常に柔軟な方法でテストを実施するのを助けます。

**Mochaのいくつかの利点**

- 簡単な非同期テスト
- 簡単なテストスイート作成
- モッキングライブラリに対する高い拡張性

### Enzyme

https://github.com/enzymejs/enzyme

EnzymeはReactテストライブラリであり、Reactアプリ開発者が出力の実行時にトラバースし、操作することを可能にします。このツールの助けを借りて、開発者はコンポーネントのレンダリング作業を行い、要素を見つけ、それらと対話することができます。EnzymeはReact用に設計されているため、マウントテストとシャローレンダリングの2種類のテスト方法を提供します。このツールはJestと共に使用されます。

**Enzymeのいくつかの利点**

- DOMレンダリングをサポート
- シャローレンダリング
- Reactフック
- 出力に対する実行時のシミュレーション

## Reactアプリをテストするにはどうすればよいのか

Reactアプリケーションをテストするために役立つ手順は以下の通りです。

### サンプル Reactアプリを構築する

APIからユーザー情報を表示する最小限のアプリケーションを作成します。このアプリケーションは、Reactアプリのテストがどのように機能するかを見るためにテストされます。

ここでは、アプリケーションのフロントエンドにのみ焦点を当てる必要があるため、`JSONPlaceholder`ユーザーAPIを使用します。

まず、以下のコマンドを実行しReactとTypeScriptのプロジェクトを生成しましょう。

```
npx create-react-app . --template typescript
```

次に、`App.tsx`ファイルを修正します

```src/App.tsx
import React, { useState, useEffect, useRef } from 'react'
import { getFormattedUserName } from './utility'
import './App.css'

// ユーザーデータの型を定義
interface User {
  id: number
  name: string
  username: string
}

const App = () => {
  const [users, setUsers] = useState<User[]>([])
  const isMounted = useRef(true) // isMountedをuseRefで管理

  // Fetch the data from the server
  useEffect(() => {
    const url: string = 'https://jsonplaceholder.typicode.com/users'
    const getUsers = async () => {
      const response = await fetch(url)
      if (!response.ok) {
        console.error('Failed to fetch users')
        return
      }
      const data: User[] = await response.json()
      setUsers(data)
    }
    getUsers()
    // Cleanup function to handle component unmounting
    return () => {
      isMounted.current = false // アンマウント時には.currentをfalseにする
    }
  }, [])

  return (
    <div>
      <h1>Users:</h1>
      <ul>
        {users.map(({ id, name, username }) => (
          <li key={id}>
            {name} -- ({getFormattedUserName(username)})
          </li>
        ))}
      </ul>
    </div>
  )
}

export default App
```

srcフォルダ内にファイルを作成します。ファイル名を`utility.ts`とし、以下の関数を記述してください。

```src/utility.ts
export const getFormattedUserName = (username: string) => {
  return "@" + username;
}
```

下記のコマンドを使用してアプリケーションを実行します

```
npm start
```

アプリケーションを実行すると、次の出力が表示されます

![スクリーンショット 2024-04-08 5.45.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/605835cd-1e28-665a-3522-325e962bebe9.png)

### ユニット（単体）テスト

アプリケーションのテストを始めましょう。

ユニット（単体）テストから始めます。
これは、個々のソフトウェアユニットやReactコンポーネントを個別にチェックするためのテストです。
アプリケーションのユニットは、ルーチン、関数、モジュール、メソッド、オブジェクトなど、何でもかまいません。
テストチームが特定の目標を設定した場合、ユニットテストは期待される結果を提供するかどうかを決定します。ユニットテストモジュールには、JestのようなReact開発ツールが提供する一連のアプローチが含まれており、テストの構造を指定します。

ユニット（単体）テストを実施するために、以下の例のようにtestやdescribeメソッドを使用できます

```tsx
describe('関数 or コンポーネント', () => {
  test('次のことを行う', () => {
    // ここにテストの結果や処理を記述します。
  })
})
```

上記の例では、testブロックがテストケースであり、describeブロックがテストスイートです。ここで、テストスイートは複数のテストケースを保持することができますが、テストケースがテストスイートに存在する必要はありません。

テストケースの中でテスターが記述するとき、彼は誤ったプロセスや成功したプロセスを検証できるアサーションを含めることができます。

以下の例では、アサーションが成功しているのを見ることができます

```tsx
describe('trueがtrueであり、falseがfalseである', () => {
  test('trueはtrueである', () => {
    expect(true).toBe(true)
  })

  test('falseはfalseである', () => {
    expect(false).toBe(false)
  })
})
```

`getFormattedUserName`関数をターゲットにすることができる最初のテストケースを作成します

```src/utility.test.tsx
import { getFormattedUserName } from './utility'

describe('utility', () => {
  test('getFormattedUserName はユーザー名の先頭に @ を追加します', () => {
    expect(getFormattedUserName('hanako')).toBe('@hanako')
  })
})
```

上記のコードに見られるように、テスト担当者はコード内でモジュールとテスト ケースを簡単に指定できるため、テストが失敗した場合でも、問題の原因を把握できます。
上記のコードが示すように、最初のテストの準備ができているので、次に行うことはテストケースを実行して出力を待ちます。

テストを実行する際は、簡単な`npm`コマンドを実行する必要があります。

```
npm test
```

次のコマンドを使用して単一のテストを実行することが可能です。

```
npm test -- -t utility
```

`create-react-app`によって作成された他のテストがある場合に実行できます。上記のコマンドを実行してすべてがうまくいけば、次のような出力が表示されます。

![スクリーンショット 2024-04-08 6.06.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/28b411e3-bb61-36c0-5d98-ea69e5497bbd.png)

出力に成功しました。

ここの出力では、1 つのテストが正常に成功したことがわかります。ただし、この場合に何か問題が発生した場合は、新しいテストを`utility`テスト条件に追加する必要があります。このためには、以下で説明するコードが役立ちます。

```src/utility.test.tsx
describe('utility', () => {
  test('getFormattedUserName は、すでに提供されている @ で始まる場合は @ を追加しません', () => {
    expect(getFormattedUserName('@hanako')).toBe('@hanako')
  })
})
```

これは異なる状況になります。
この場合、ユーザー名の文字列の先頭に @ 記号がすでに含まれている場合、関数は他の記号なしでユーザー名が指定されたときの出力を返します。同じ出力は次のとおりです。

![スクリーンショット 2024-04-08 6.10.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/8157d85d-e393-8b13-fc7d-ce98d2fe7ef0.png)

テスト出力が失敗しました。

予想通り、出力として期待された値と違っていたため、テストは失敗しました。ここで、問題を検出できた場合は、次のコードを使用して問題を修正することもできます。

```src/utility.ts
export const getFormattedUserName = (username: string) => {
  return !username.startsWith('@') ? `@${username}` : username
}
```

このコードの出力は次のようになります。

![スクリーンショット 2024-04-08 6.14.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/d39ccc82-0204-0b8e-a59a-608e23f09ec9.png)

ご覧のとおり、テストは成功しました。

### スナップショットのテスト

次に、スナップショットテストという別のテストタイプについて説明します。このテストタイプは、アプリケーションのUIが予期せず変更されないことを確認したい場合に使用されます。

スナップショットテストは、アプリケーションのUIコンポーネントをレンダリングし、そのスナップショットを撮影し、参照用のファイルに格納されている他のスナップショットと比較するために使用されます。ここで、2つのスナップショットが一致すれば、テストは成功したということです。そうでない場合は、コンポーネントに予期せぬ変更があった可能性があります。この方法を使用してテストを記述するには、`react-test-renderer`ライブラリが必要です。
これにより、Reactアプリケーションのコンポーネントをレンダリングできます。

`react-test-renderer`ライブラリをインストールすることです。typescriptを利用するので、`@types`もインストールします。

```
npm install react-test-renderer
npm install --save-dev @types/react-test-renderer
```

ファイルを編集してスナップショットテストを含めます。

```src/App.test.tsx
test('check if it renders a correct snapshot', async () => {
  let tree = renderer.create(<App />)
  await act(async () => {
    tree.update(<App />)
  })
  expect(tree.toJSON()).toMatchSnapshot()
})
```

テスターが上記のコードを実行すると、次の出力が得られます。

![スクリーンショット 2024-04-08 6.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/a2276e95-e879-e5da-4db5-682aeee88849.png)

このテストを実行すると、Jest などのテストランナーがスナップショットファイルを作成し、それをスナップショット（`__snapshots__`）フォルダーに追加します。どのように見えるかは次のとおりです。

```src/__snapshots__/App.test.tsx.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`check if it renders a correct snapshot 1`] = `
<div>
  <h1>
    Users:
  </h1>
  <ul>
    <li>
      Yamada Hanako
       -- 
        (
        @Yamada
        )
    </li>
  </ul>
</div>
`;
```

ここで、App コンポーネントを変更したい場合、変更する必要があるのは1つのテキスト値だけです。ここでは、出力に変更が生じるため、正しいスナップショットテストは失敗すると表示されます。

![スクリーンショット 2024-04-08 6.36.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/c1d65fef-444d-03b8-b04e-c2749e9b7759.png)

テストを成功させるには、意図的な変更について`Jest`などのツールに通知する必要があります。これは、`Jest`が監視モードの場合に簡単に実行できます。以下に示すようなスナップショットが取得されます。

![スクリーンショット 2024-04-08 6.38.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42432/383b1875-c3ee-9c2b-8981-623f4d967912.png)


### エンドツーエンドのテスト

エンドツーエンド（E2E）テストは、Reactアプリケーションなどのソフトウェア開発において非常に重要なテストの一つです。このテストでは、アプリケーションが実際の使用環境でどのように機能するかを全体的にチェックします。つまり、ユーザーの視点からアプリケーションの全ての機能を試すことが目的です。エンドツーエンドテストを通して、アプリケーションのすべての複雑さや依存関係を考慮して、システム全体の動作を検証します。

エンドツーエンドテストの主な特徴としては、システム全体が含まれることです。これにはフロントエンドからバックエンド、データベースまで、アプリケーションが依存しているすべての外部サービスやAPIが含まれます。一連の処理（例えば、ユーザーがアプリケーションにログインして、特定の情報を表示して、何かの操作を行う等）を実際に行い、期待通りに動作するかどうかを検証します。

エンドツーエンドテストには様々なアプローチがあり、主な方法としては以下のようなものがあります。

- **自動化されたテスト**: 特定のテストフレームワークやツールを用いて自動的にテストを行います。この方法は時間とコストの節約になり、反復可能なテストを可能にします。
- **マニュアルテスト**: 実際に人間がアプリケーションを操作し、機能が期待通りに動作するかをチェックします。より柔軟性がありますが、時間がかかる可能性があります。

エンドツーエンドテストは、アプリケーションのリリース前に最終的な品質保証を行うための重要なステップです。

### 統合テスト

複数のモジュールやコンポーネントが連携して正しく機能することを確認するプロセスです。
Reactアプリケーションのコンテキストでは、このテストは異なる部分のコード（例えば、異なるReactコンポーネントやAPI呼び出しなど）が一緒に動作するときの挙動を検証するために行います。

このために、ソフトウェアテストチームは以下の手順に従う必要があります。

追加で以下のパッケージをインストールします

```
npm install --save-dev jest jest-dom nock jest-fetch-mock
```

App.tsxのファイルにGitHubのユーザーネームを入力し、該当するGitHubユーザーのリポジトリ一覧取得機能を追加したので確認しましょう。

```src/App.tsx
import React, { useState, useEffect, useRef } from 'react'
import { getFormattedUserName } from './utility'
import './App.css'

interface Repo {
  id: number
  name: string
}

const App = () => {
  // 既存のコード...

  const [username, setUsername] = useState<string>('')
  const [repos, setRepos] = useState<Repo[]>([])
  const [error, setError] = useState<string>('')

  const fetchRepos = async (username: string) => {
    setRepos([])
    setError('')
    try {
      const response = await fetch(
        `https://api.github.com/users/${username}/repos`,
      )
      if (!response.ok) {
        throw new Error('ユーザーが見つかりません')
      }
      const data: Repo[] = await response.json()
      setRepos(data)
    } catch (error: any) {
      setError(error.message)
    }
  }

  const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUsername(e.target.value)
  }

  const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
    if (e.key === 'Enter') {
      fetchRepos(username)
    }
  }

  return (
    <div>
      // 既存のコード...
      <input
        type="text"
        placeholder="GitHub username"
        value={username}
        onChange={handleInputChange}
        onKeyDown={handleInputKeyDown}
      />

      {error && <p>{error}</p>}

      <ul>
        {repos.map((repo) => (
          <li key={repo.id}>{repo.name}</li>
        ))}
      </ul>
    </div>
  )
}

export default App
```

次に、`viewGitHubRepositoriesByUsername.test.ts`という名前の統合テストスイートファイルを作成します。この場合、`Jest`が自動的に取得してくれるので便利です。

コードを使用して依存関係をインポートします。

```src/viewGitHubRepositoriesByUsername.test.tsx
import React from 'react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import App from './App'
import '@testing-library/jest-dom/extend-expect'
import fetchMock from 'jest-fetch-mock'
```

コードに従ってテストスイートを設定します。

```src/viewGitHubRepositoriesByUsername.test.tsx
// Jestのグローバル設定にfetchMockを有効化
fetchMock.enableMocks()

beforeEach(() => {
  // 各テストの実行前にfetchのモックをリセット
  fetchMock.resetMocks()
})

test('renders user list correctly', async () => {
  fetchMock.mockResponseOnce(
    JSON.stringify([
      { id: 1, name: 'Yamada Hanako', username: 'Yamada' },
      { id: 2, name: 'Tanaka Taro', username: 'Tanaka' },
    ]),
  )

  render(<App />)

  await waitFor(() => {
    expect(screen.getByText('Yamada Hanako -- (@Yamada)')).toBeInTheDocument()
    expect(screen.getByText('Tanaka Taro -- (@Tanaka)')).toBeInTheDocument()
  })
})

test('fetch and display user repos upon submitting a username', async () => {
  fetchMock.mockResponses([
    JSON.stringify([
      { id: 1, name: 'repo1' },
      { id: 2, name: 'repo2' },
    ]),
    { status: 200 },
  ])

  render(<App />)

  // ユーザー名を入力
  userEvent.type(screen.getByPlaceholderText('GitHub username'), 'test{enter}')

  // レポジトリが表示されるのを待つ
  await waitFor(() => {
    expect(
      screen.getByText((content) => content.startsWith('repo1')),
    ).toBeInTheDocument()
    expect(
      screen.getByText((content) => content.startsWith('repo2')),
    ).toBeInTheDocument()
  })
})

```

## まとめ

今回使用したコードをgithubにあげています。少しでも参考になれば幸いです。

https://github.com/andmorefine/react-app-testing-jest

Reactアプリケーションのテストを行うためには、まずチームがReactのテストに関連するライブラリやツールに関する知識を持つことが重要です。
それができたら、次に行うべきことは、自分たちのプロジェクトに必要なテストの種類を正確に把握することです。それぞれのテストには特定の目的があるため、プロジェクトのニーズに合わせて、最も適したテストツールを選ぶことができます。

### 参考資料

- [Reactアプリをテスト](https://jestjs.io/ja/docs/tutorial-react)
- [React テストコード（Jest, React Testing Library）の学びと Tips](https://qiita.com/t-kurasawa/items/950a0f4ccf57312d6430)
- [今日からはじめるReactのテスト戦略](https://zenn.dev/t_keshi/articles/react-test-rule)
- [【React】フロントエンド自動テストのための事前知識](https://gizumo-inc.jp/media/react-testing-technique/)