React Samplesってサイトが、テストコードの実装練習に役立っている

以下のサイトをご存知でしょうか。

https://reactjsexample.com/

私は2023年になったタイミングくらいで見つけた気がします。
どういった経路で皆さんが公開しているかまでは知りませんが、皆さんが、自分が作成されたサイトについて、概要を紹介するページと、githubのRepo情報が掲載されています。

## React Samplesを持ち出した訳
個人的に（個人開発）でテストコードに対する理解を深めたいときや現場で書くことになりそうだけど、規模が大きくて・・・もう少し小規模なアプリケーションに対するテストを実装したい場合に、React Samplesが役立つことがあると思います。

アプリケーションコード実装としても参考になる内容はたくさんありますが、
**今回は、こちらのサイトのアプリケーションコードをお借りすれば、テストコード学習がスムーズにできる気がしたので、そこをまとめてみました。**

## React Testing Libraryとテスト

https://testing-library.com/docs/

Reactのテスト実装で登場するライブラリです。

実際にユーザがページを開いて行う操作を、コードベースに落とし込んで、機能の保証に役立つライブラリ、とも言えます。（以下の文章は、上記リンクの内容を抜粋しています。）
> The main utilities it provides involve querying the DOM for nodes in a way that's similar to how the user finds elements on the page. In this way, the library helps ensure your tests give you confidence that your application will work when a real user uses it.

:::note 
**仕様理解でも役立つテストコード**

実装自体と直接は関係しませんが、テストコードを眺めるのは、**仕様に対する理解を深めることに役立つ**と思っています。
ドキュメント（Wiki?）やExcelでも仕様は理解できますが、テストコードを確認することで、どういった状況でどんな挙動をするか、具体的に見えてきます。
テストコードに全幅の信頼を寄せはしませんが、ドキュメントで発生しうる「更新漏れ」よりは信頼できると思います。。。

:::

#### jestによるテスト実装
テストコードの実装をすれば、必ず登場するライブラリです。

https://jestjs.io/ja/

Jest コアチームの言葉をお借りします。
> Jest はあらゆる JavaScript のコードベースの正しさを保証するために設計された JavaScript テスティングフレームワークです。親しみやすく、豊富な機能を持つAPIによって簡単にテストを書くことができ、さらには素早く結果を得ることができます。
Jest はドキュメントが豊富に用意されているほか、設定はほとんど不要です。さらに、要件に合わせて拡張することができます。Jest はテストを楽しいものにします。

#### テスト実施に必要なライブラリ
テストコードを実装するために必要なライブラリがいくつかありますが、`create-react-app`を使ってプロジェクトを作成した場合、
`package.json`に、`@testing-library/XXX`のライブラリが最初から設定されています。
```json:package.json
{
  "name": "airbnb-clone",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^13.4.0",
    "@testing-library/user-event": "^13.5.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "web-vitals": "^2.1.4"
  },
/*このあたりは省略*/
}
```
## テストを書いてみる
以下の三段階でテストコードを書いてみました。
- 表示文字列のテスト
- Eventを用いたテスト
- TODOアプリのテスト

:::note 

テストコードを実装してみて、本当に検証できているのか？漏れはないのか？
考えるのが大変な場合もありますが、いきなりそれを考えてしまうとなかなか進まないと思います。

ですがまずはとりあえず書いてみて、**この挙動に対するテストはできたかも！と思う段階に進むだけでもステップアップではないでしょうか！**
軽い気持ちで始めてみましょう！！

:::

#### 表示文字列のテスト
比較的直感的に実装ができそうな内容で、テストコードの中でも初級編、だと思っています。
アプリケーションを起動し、「画面に表示される文言があるときに、その文言が表示されることを期待値とする」テストケースですね。

https://reactjsexample.com/airbnb-website-clone-using-react/

実際の画面は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3435699d-c5a0-de24-937b-da5ec0c06ead.png)

上記画面に関するアプリケーションコードとテストコードを並べてみました。
```react:Hero.js(アプリケーションコード)
import React from "react";
import photoGrid from "../img/photoGrid.png"

export default function Hero() {
    return (
        <section className="hero">
            <img src={photoGrid} className="hero--photo" alt="Photo Grid"></img>
            <h1 className="hero--title">Online Experiences</h1>
            <p className="hero--subtitle">Join unique interactive activities led by one-of-a-kind hosts—all without leaving home.</p>
        </section>
    )
}
```

```react:Hero.test.js(テストコード)
import { render, screen } from '@testing-library/react';
import Hero from './Hero';

test('Hero', () => {
    render(<Hero />);
    screen.debug(); // ※1
    const displayedImage = document.querySelector("img");
    expect(displayedImage.src).toContain("photoGrid"); // ※2
    const linkElement = screen.getByText(/Online Experiences/i);
    expect(linkElement).toBeInTheDocument(); // ※3
});
```
**※1** `screen.debug()`：実行したタイミングのDOMを表示します。
<details><summary>コンソールの内容</summary>

```html
  <body>
    <div>
      <section
        class="hero"
      >
        <img
          alt="Photo Grid"
          class="hero--photo"
          src="photoGrid.png"
        />
        <h1
          class="hero--title"
        >
          Online Experiences
        </h1>
        <p
          class="hero--subtitle"
        >
          Join unique interactive activities led by one-of-a-kind hosts—all without leaving home.
        </p>
      </section>
    </div>
  </body>
```
</details>

**※2** 画像ファイルに関するテストの実装方法は[**こちら**](https://medium.com/@drake_beth/how-to-test-images-in-react-a70053b1634a)を参考にしました。

**※3** `expect(linkElement).toBeInTheDocument();`でエラーが発生する
記載したテストコードをそのまま動化した場合、以下のエラーが表示されます。
```
TypeError: expect(...).toBeInTheDocument is not a function
```
**import文が不足していることにより、発生しているエラーです。**
`expect(...).toBeInTheDocument()`を使うファイルで
`import '@testing-library/jest-dom/extend-expect';`と書けば解消されますが、
期待値に関する処理はどのケースでも書くので、
全ファイルに、このimport文を記述する羽目になります。。。

そこで**srcフォルダに、`setupTests.js`を作成し、** そこでimport文を定義します。
これで、各ファイルでimport文は記述せずに済みます。
```js:setupTests.js
import '@testing-library/jest-dom/extend-expect';
```

---

#### Eventを用いたテスト
いきなりTodoアプリのテストするの？難しくない？といった方は、ClickイベントとuseStateだけの機能、カウンターなどを実装してみるといいかと思います。
こちらはReact Samplesのコードは関係なく、私の手元で作成したものです。

```react:Counter.js(アプリケーションコード)
import { useState } from "react"

export default function Counter() {
    const [count, setCount] = useState(0)
    const increment = () => {
        setCount(count => count + 1)
    }

    return (
        <>
            {count}
            <button onClick={increment}>Increment</button>
        </>
    )
}
```

```react:Counter.test.js(テストコード)
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('Counter', () => {
    render(<Counter />);
    const count = screen.getByText(/0/i);
    // 初期表示では0であること
    expect(count).toBeInTheDocument();
    const buttonElement = screen.getByRole("button");
    fireEvent.click(buttonElement)
    const countUpdate = screen.getByText(/1/i);
    // 1に更新されること
    expect(countUpdate).toBeInTheDocument();
});
```

---

#### TODOアプリのテスト
以下の記事に記載されていたアプリケーションのコードを使いました。

https://reactjsexample.com/solution-for-a-challenge-from-devchallenges-io/

**デモ**

https://todo-app-one-topaz.vercel.app/

**Nextjsの場合**
今回使用したTODOアプリケーションは、Reactではなく、Nextjsでした。
create-react-appで作成したアプリケーションにはデフォルトで`@testing-library`がインストールされていますが、nextjsの場合は違うので、設定から行います。

https://nextjs.org/docs/pages/building-your-application/optimizing/testing

以下リポジトリの`package.json`を参考に、必要なライブラリ情報を確認します。

https://github.com/vercel/next.js/tree/canary/examples/with-jest

```
npm i -D @testing-library/jest-dom @testing-library/react @testing-library/user-event jest jest-environment-jsdom
```

```javascript:jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
    // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
    dir: './',
})

// Add any custom config to be passed to Jest
const customJestConfig = {
    setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
    testEnvironment: 'jest-environment-jsdom',
}

// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
module.exports = createJestConfig(customJestConfig)
```

```javascript:jest.setup.js
// Optional: configure or set up a testing framework before each test.
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`

// Used for __tests__/testing-library.js
// Learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'
```

ここでは、2つのケースに関するテストコードを紹介します。
- **テキストボックスに値を入力して、タスクを追加**
- **追加したタスクに対してチェックをつける/外す**

他のテストコードは、以下リポジトリで公開しています。

https://github.com/eno-conan/react-samples-write-tests

**テキストボックスに値を入力して、タスクを追加**

アプリケーションコードは以下です。

https://github.com/GustavoPendeza/todo-app/blob/master/src/components/Todo.tsx

```react:Todo.test.tsx(テストコード)
import Home from '@/app/page'
import { fireEvent, render, screen } from '@testing-library/react'

describe('Home', () => {
    it('Allタブを表示しているとき、タスク追加ができること', () => {
        render(<Home />)

        const inputElement = screen.getByPlaceholderText(/add details/i) as HTMLInputElement;

        fireEvent.change(inputElement, {
            target: { value: 'Learn Testing Library' },
        });
        // テキストボックスの値が更新できること
        expect(inputElement.value).toBe('Learn Testing Library');

        const onButtonElement = screen.getByRole('button', { name: 'Add' });
        fireEvent.click(onButtonElement);
        //タスクが追加が行えること
        const helloTask = screen.getByText('Learn Testing Library')
        expect(helloTask).toBeInTheDocument();

    })
})
```
※`fireEvent`を使って、画面に対するイベントを実行していますが、
`userEvent`を使った方がいい場合もあるらしいです。（今回はその検証はやっていません）

https://ph-fritsche.github.io/blog/post/why-userevent

**追加したタスクに対してチェックをつける/外す**

アプリケーションコードは以下です。

https://github.com/GustavoPendeza/todo-app/blob/master/src/app/page.tsx

```react:page.test.tsx(テストコード)
import Todo from '@/components/Todo'
import { fireEvent, render, screen } from '@testing-library/react'

describe('Home', () => {

    // 親コンポーネントの関数(deleteTodo)のモック
    const mockFunction = jest.fn();
    // useStateのモック
    const mockSetXXX = jest.fn();

    jest.mock('../app/page', () => ({
        default: () => ({
            deleteTodo: mockFunction,
            setReload: mockSetXXX,
        }),
    }));

    it('renders task and check task', () => {
        // サンプルタスクを設定
        render(<Todo
            menuActive={'all'}
            todo={{
                id: '1',
                text: 'Sample Task',
                checked: false
            }}
            reload={false}
            setReload={mockSetXXX}
            deleteTodo={mockFunction} />
        )

        const sampleTaskCheckBox = screen.getByRole('checkbox') as HTMLInputElement;
        // チェックボックスを更新
        fireEvent.click(sampleTaskCheckBox);
        expect(sampleTaskCheckBox.checked).toBe(true);
        // チェックボックスを更新
        fireEvent.click(sampleTaskCheckBox);
        expect(sampleTaskCheckBox.checked).toBe(false);
    })
})
```
今回のサンプルの場合、他に以下のようなテストケースがありそうです。
- AllからActiveやCompletedタブに切り替えたときの表示内容
- Completedタブ：ごみ箱アイコンクリック
- Completedタブ：`delete all`ボタンをクリック

:::note warn
**テストコードの精度**

Todoアプリケーションの規模になると、実装方法で差がでてくると思います。
実装方法次第でテストコードの書きやすさが変わる可能性があります。
どういったアプリケーションコードにすれば、書きやすいテストコードになるのか。実装したテストコードが果たして適切なものなのか。

個人で判断するには限界があり、より優れた形のテストを作りあげる上で、他の方の意見が必要な場合があります。

今回は、**React Samplesのアプリケーションコードを材料にすれば、テストを書くための材料はそろっていますよ、といった話がメインです。**
当然私自身も、テストコードについて、検証したいことは確かめられる形で実装したつもりですが、もっと良い形があるかもしれない、と思いつつ書いている部分もあります。

:::

Jestのテストの書き方は、以下記事が参考なりました。

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

また、今回のサンプルは、ルートパスのみでアプリケーションでしたが、
あるパスから別のパスへ画面遷移させる場合もあるでしょう。

https://note.com/cyberz_cto/n/n3ba47e0dcfd2

### まとめ
React Samplesというサイトにアップされたコードを土台に、テストコードを書いてみる、といった話をしてきました。

今回はReact-test-libraryを用いて話をしてきましたが、Viteとvitestによる構成もあるはずです。そちらも見てみようと思っているところです。

私も現場で多少はテストコードを実装していますが、まだまだなので、
少しずつ知識を増やしつつ、実装しつつ。日々取り組んでいこうと思います。

ありがとうございました。