JestでReact（TypeScript）のテストしてみた

# :pushpin:　はじめに
Reactで個人的に色々作ったり勉強することが多く、「テストまで回す癖つけた方がいいよ！」というアドバイスをいただいたので、初めてのReactでのテストの備忘録として書いてます。なので初歩的な内容です。。。

# :pushpin:　Jestとは
それぞれのプログラミング言語には、テストを補助するためのフレームワークが存在します。
Jestは、JavaScriptのテストフレームワークです。
Babel、TypeScript、Node、React、Angular、Vue などの様々なフレームワークを利用したプロジェクトで動作します！（公式引用）

**Jest公式：[https://jestjs.io/ja/](https://jestjs.io/ja/)**


# :pushpin:　とりあえず、テストファイルを見てみる
### ① Reactのプロジェクトを作る
```shell
npx create-react-app my-app --template typescript
```
プロジェクトを作ると、自動的に
- `App.test.tsx`
- `setupTests.ts`

というファイルが作成されます。

### ② App.test.tsxを見てみる
**App.test.tsx**には既に、下記のようなコードが書かれています。
```typescript:App.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});
```
上記では、`Appコンポーネント`のテストを行っています。

### ③ setupTests.tsを見てみる
テストの初期化設定をするファイルです。
[`create react app`](https://create-react-app.dev/docs/running-tests/#srcsetuptestsjs)のサイトにsetupTests.jsについて書いてあります。
> この機能は、react-scripts@0.4.0 以降で利用可能です。
> アプリがテストでモックする必要のあるブラウザAPIを使用している場合、またはテストを実行する前にグローバルな設定が必要な場合は、プロジェクトに src/setupTests.js を追加してください。これは、テストを実行する前に自動的に実行されます。

**setupTests.ts**ファイルには、下記のように書かれています。
```typescript:setupTests.ts
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
```
※ `アサーション`とは、あるコードが実行される時に満たされるべき条件を記述して実行時にチェックする仕組み。（←自分がわからなかったので、自分用に記述）

### ④ テストを走らせてみる
```shell
npm test
```
すると、下記のように表示されるので、とりあえず`aキー`を押して全てのテストを実行します。
```shell
No tests found related to files changed since last commit.
Press `a` to run all tests, or run Jest with `--watchAll`.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

ーーー

// 実行結果
 PASS  src/App.test.tsx
  ✓ renders learn react link (44 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.308 s
Ran all test suites.

Watch Usage: Press w to show more.
```
元々のソースを何もいじってないので通ります。
`wキー`を押し、`qキー`を押してウォッチモードを終了します。


# :pushpin:　Jestについて理解する
### ①　テストの書き方
```typescript
test('テストケースの名前', () => {
    // テスト
});
```
### ②　関連テストをまとめる
`describe()`を使うことで、いくつかの関連するテストをまとめたブロックを作成することができます。
```typescript
describe('ブロック名', () => {
  test('テストケース1', () => {
      // テスト
  });
  test('テストケース2', () => {
      // テスト
  });
});
```

参考：[https://jestjs.io/ja/docs/api#describename-fn](https://jestjs.io/ja/docs/api#describename-fn)
### ③　テストの条件を定義する
`Matcher（マッチャー）`を使って、テストの評価条件を定義します。
Matcherはメソッドです。テスト実行時に、どんな結果が返ってきてほしいかを定義して、失敗成功を判定するようです。
下記のように書きます。
```typescript
expect(値).マッチャー();
```
`expect(値)`は、"expectation" オブジェクトを返します。
expect関数には、テストしたい関数やコンポーネントを渡してあげます。
```typescript:App.test.tsx
// テストしたい関数
function calcFactorial(num: number):number {
  let result: number = 1;
  for(let i = num; i > 0;i--) {
      result = result * i;
  }
  return result;
}

// テスト
test('/utils/calc.tsのcalcFactorial関数のテスト', () => {
  const num: number = 4;

  console.log(calcFactorial(num));
  expect(calcFactorial(num)).toStrictEqual(24);
});
```
Matcherの一覧やそれぞれのメソッドの使い方をわかりやすくまとめてくれているサイトがたくさんありますので、そちらを参考にしてみてください。

::: note 
**参考サイト**
- [【Jest超基礎】一般的なマッチャーの使い方](https://ramble.impl.co.jp/565/)
- [testing-library/jest-dom(github)](https://github.com/testing-library/jest-dom)
- [Jest再入門 - マッチャー編](https://developer.mamezou-tech.com/testing/jest/jest-matchers/)
:::

# :pushpin:　テスト書いてみる
フォルダ構成は以下のようにしました。
```
src
 L App.css
 L App.test.tsx
 L App.tsx
 L index.css
 L index.tsx
 L logo.svg
 L react-app-env.d.ts
 L reportWebVitals.ts
 L setupTests.ts
 L __tests__                    <<- 追加
     L list.test.tsx            <<- 追加
 L components                   <<- 追加
     L list                     <<- 追加
         L list.module.css      <<- 追加
         L list.tsx             <<- 追加
 L utils                        <<- 追加
     L calc.ts                  <<- 追加
```
### ① TypeScriptの簡易的なテスト
まずは、「TypeScriptで書いた関数が意図した結果を返してくれるかどうか」くらいのテストをやってみます。

#### まずテスト用の関数の作成
配列を受け取り、配列中に2つ以上発生する数値を除去した配列を返す関数を作ります。（語彙力が乏しくてすみません）
```typescript:./utils/calc.ts 
function removeDuplicates(arr: number[]):number[] {
    let result:number[] = [];

    arr.forEach((num, i) => {
        let count = 0;

        arr.forEach((num2) => {
            if(num === num2) count ++;
        });
        // 配列中に同じ値が1つだけ出現したやつをresult配列に
        if(count === 1) result.push(num);
    });

    return result;
}

module.exports = removeDuplicates;
```

#### テストを書く
とりあえず、`App.test.tsx`に記述していきます。
```typescript:App.test.tsx
test('/utils/calc.tsのremoveDuplicates関数のテスト', () => {
    const numArr: number[] = [3, 4, 2, 6, 2, 2, 1, 3, 4, 5, 7];
    const removeDuplicates = require('./utils/calc');
  
    console.log(removeDuplicates(numArr));
    expect(removeDuplicates(numArr)).toStrictEqual([6, 1, 5, 7]);
});
```
:::note
Reactでテストって何から書けばいいのか正直わからなかったのですが、
「`console.log`などで変数の中身を表示する時ってあるじゃない？
エラー出たら出力する為に書いて、解決して消して、またエラー出たから書いて、、、ってやっていると面倒だから、console.logをテストに書いておいて、テスト回して出力確認するっていうのから初めてみたらいいのよん！」
とアドバイスをいただき、個人学習でテストを行うハードルがかなり下がりました:angel_tone2:
:::
結果はこんな感じ↓ `wキー`押して、`qキー`押してウォッチモードを終了します。
```shell
  console.log
    [ 6, 1, 5, 7 ]

      at Object.<anonymous> (src/App.test.tsx:16:11)

 PASS  src/App.test.tsx
  ✓ renders learn react link (47 ms)
  ✓ /utils/calc.tsのremoveDuplicates関数のテスト (53 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.627 s
Ran all test suites related to changed files.

Watch Usage: Press w to show more.
```

#### 引っ掛かったらこんな感じ
テスト時に使うMatcher間違えた時のエラーです！
通ったテストと、通らなかったテストがわかりやすいですね！
```shell
 FAIL  src/App.test.tsx
  ✓ renders learn react link (46 ms)
  ✕ /utils/calc.tsのremoveDuplicates関数のテスト (27 ms)

  ● /utils/calc.tsのremoveDuplicates関数のテスト

    expect(received).toBe(expected) // Object.is equality

    If it should pass with deep equality, replace "toBe" with "toStrictEqual"

    Expected: [6, 1, 5, 7]
    Received: serializes to the same string

      14 |   const removeDuplicates = require('./utils/calc');
      15 |   
    > 16 |   expect(removeDuplicates(numArr)).toBe([6, 1, 5, 7]);
         |                                    ^
      17 | });

      at Object.<anonymous> (src/App.test.tsx:16:36)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        3.594 s
Ran all test suites related to changed files.
```

### ② コンポーネントのテスト
「コンポーネントが意図したように表示されるかどうか」のテストをやってみます。

::: note
**コンポーネントのテスト観点**
下記のサイトがすごく参考になりました。
- [コンポーネント（単体、ユニット）テストとは？手法などを例で紹介](https://service.shiftinc.jp/column/3636/)
- [サバイバルTypeScript： Reactコンポーネントのテストを書こう](https://typescriptbook.jp/tutorials/component-test#testing-library%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%9F%E3%83%86%E3%82%B9%E3%83%88%E3%81%AE%E4%BD%9C%E3%82%8A%E6%96%B9%E3%81%A8%E3%82%84%E3%82%8A%E6%96%B9)
:::

#### まずテスト用のコンポーネントの作成
propsで`items`、`headerData`を受け取り、リストを表示するコンポーネントです。
`headerData`が渡された時は、リストにヘッダーを表示するようにしています。
```typescript: components/list/list.tsx
import React from "react";

/* import css */ 
import styles from './list.module.css';

type Props = {
    items: {title: string, description?: string, icon?: React.ReactNode}[],
    headerData?: string
}

export default function ListWithIcon(props: Props){

    const {items, headerData} =  props;

    return(
        <ul className={styles.listWrapper}>
            { headerData && <li className={styles.header}> { headerData } </li> }
            {
                items.length === 0 ? 
                    <span>表示するデータがありません。</span>
                :
                    items.map((item, index) => {
                        return(
                            <li key={index}>
                                <span className={styles.leftContent}>
                                    {item.icon && 
                                        <span className={styles.iconWrapper}>
                                            { item.icon }
                                        </span>
                                    }
                                    <span className={styles.title}>{item.title}</span>
                                </span>
                                <span>{item.description}</span>
                            </li>
                        )
                    })
            }
        </ul>
    )
}

```
```css: components/list/list.module.css

.listWrapper {
    padding: 0;
}

.listWrapper li {
    padding: 0.5rem 1rem;
    border-radius: var(--my-border-radius-s);
    background-color: var(--my-color-bgColor);
    list-style: none;
    margin-bottom: 0.5rem;
    color: var(--my-color-navy);
    transition: all 0.5s;
    display: flex;
    align-items: center;
    justify-content: space-between;
}

.listWrapper li:not(.header):hover {
    opacity: 0.5;
    transition: all 0.5s;
}

.listWrapper .header {
    background-color: var(--my-color-navy);
    color: var(--my-color-white);
    border-radius: var(--my-border-radius-s) var(--my-border-radius-s) 0 0;
}

.leftContent {
    display: flex;
    align-items: center;
    justify-content: left;
}

.title {
    font-weight: bold;
    margin: auto var(--my-margin-s);
}
.iconWrapper {
    background-color: var(--my-color-green);
    border-radius: var(--my-border-radius-circle);
    color: var(--my-color-white);
    min-width: var(--my-font-size-h1);
    min-height: var(--my-font-size-h1);
    padding: var(--my-padding-xs);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: var(--my-font-size-p);
}

```
#### テストを書いてみる
新しく`__tests__`の中に`list.test.tsx`ファイルを追加し、テストを記述します！
今回テストしたい内容は下記になります。
「`headerDataを渡した場合はリストにヘッダーを表示し、渡さない場合は非表示になるかどうか`」
なので、
:point_up: headerDataを渡して**コンポーネントを描画**し、**ヘッダーが表示されるか確認**するパターン　と
:v: headerDataを渡さずに**コンポーネントを描画**し、**ヘッダーが非表示になるか確認**するパターン　の
2つのテストを記述します！！
```typescript: /__tests__ / list.test.tsx
import { render, screen } from '@testing-library/react'
import ListWithIcon from '../component/list/list';
import CategoryIcon from '@mui/icons-material/Category';
 

describe('components/listテスト', () => {

    // ListWithIconコンポーネントに渡すデータ
    const listItems: {title: string, description?: string, icon?: React.ReactNode}[] = [
        {title: 'タイトル1'},
        {title: 'タイトル2', description: '説明がきです〜'},
        {title: 'タイトル3', description: 'ディスクリプションです〜', icon: <CategoryIcon fontSize="small" />}
    ]

    /* --  テスト１ -- */
    test('headerDataを渡しヘッダーを表示する', () => {
        // render()関数を使ってコンポーネントを描画（headerDataを渡す）
        const view = render(<ListWithIcon items={listItems} headerData='リストのヘッダーだよ'/>);
        
        // 興味本位でconsole.log
        console.log('view: ', view);
        // 要素の取得（@testing-library/reactのクエリ ` queryByTestId ` を使用）
        const listDom = screen.queryByTestId('list-header');
        // ちゃんと返ってくる
        console.log(listDom);
        // nullにならないことを期待する
        expect(listDom).not.toBeNull();
    })

    /* -- テスト２ -- */
    test('headerDataを渡さずヘッダーを表示しない', () => {
        // render()関数を使ってコンポーネントを描画（headerDataを渡さない）
        render(<ListWithIcon items={listItems} />);
        // 要素の取得（@testing-library/reactのクエリ ` queryByTestId ` を使用）
        const listDom = screen.queryByTestId('list-header');
        // nullが返ってくる
        console.log(listDom);
        // nullになることを期待する
        expect(listDom).toBeNull();
    })

})
```
特定のDOMを取得には、`getByRole()`メソッドを使用するのですが、今回自分のコンポーネント定義の仕方が悪くて、リストのヘッダーもそれ以外もliタグで描いちゃってるので、getByRole('listitem')で指定しちゃうと、`TestingLibraryElementErrorです： 役割 "listitem" を持つ要素が複数見つかりました。`と怒られちゃいました。
複数要素を探す方法もあるのですが、今回はヘッダーにあたるliの表示非表示をテストしたいということで、色々調べて、あまり推奨されてはいませんが、とりあえず挙動を確認したいので`getByTestId()`を使うことにしました。
テストで利用したい要素にdata-testid属性に設定した任意の名前でDOMを取得することができます。

:::note
**Testing Libraryのクエリについて**
```typescript
sceren.クエリ();
```
>ページ上の要素を見つけるためにTesting Libraryが提供するメソッドです。クエリにはいくつかの種類（「get」、「find」、「query」）があり、それらの違いは、要素が見つからなかった場合にエラーを投げるか、Promiseを返して再試行するかです。どのようなページのコンテンツを選択するかによって、異なるクエリがより適切であったり、より適切でなかったりすることがあります。最もアクセスしやすい方法でページをテストするためにセマンティッククエリを利用する方法に関する推奨事項については、優先順位ガイドを参照してください。
>
>引用：https://testing-library.com/docs/queries/about
:::

:::note warn
const 任意の名前 = render(コンポーネント); とすると、下記のようなエラーが出ました。
戻り値の名前は、`view`または`utils`が推奨されているようです。
>`任意の名前` is not a recommended name for `render` returned value. Instead, you should destructure it, or name it using one of: `view`, or `utils`
:::

:::note warn
**getByTestId()について**
[`Testing Library`](https://testing-library.com/docs/queries/bytestid/)のサイトには、以下のように記載がありました。
> In the spirit of [the guiding principles](https://testing-library.com/docs/guiding-principles), it is recommended to use this only after the other queries don't work for your use case. Using data-testid attributes do not resemble how your software is used and should be avoided if possible. That said, they are way better than querying based on DOM structure or styling css class names. Learn more about data-testids from the blog post "[Making your UI tests resilient to change](https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change)"
>> 指導原則の精神に則り、他のクエリがあなたのユースケースで機能しない場合にのみ、これを使用することが推奨されます。data-testid属性の使用は、あなたのソフトウェアがどのように使用されているかに似ていないので、可能であれば避けるべきです。とはいえ、DOM構造やスタイリングCSSのクラス名に基づいてクエリを実行するよりは、はるかに優れています。data-testidについては、ブログポスト「[Making your UI tests resilient to change](https://kentcdodds.com/blog/making-your-ui-tests-resilient-to-change)」で詳しく解説しています。
:::


# 最後に
次回は、`setupTest.ts`の記述方法やイベントを使ったテスト、結合テストあたりを記事にできるくらい理解してこようと思います。


# 参考資料

https://jestjs.io/ja/docs/api#describename-fn

https://zenn.dev/koki_tech/articles/a96e58695540a7

https://and-engineer.com/articles/YZW_IhEAACIAdlzA

https://engineering.mercari.com/blog/entry/20211208-test-automation-policy-in-merpay-frontend/

https://zenn.dev/takepepe/articles/nextjs-testing-strategy-2022

https://reffect.co.jp/react/react-test