React初心者がテスト駆動開発でTodoアプリを作ってみた

# はじめに
React、テスト駆動開発共に初心者である私が、それを使うとどういうメリット、デメリットがあるのかを理解しながらTodoアプリの作成を行いました。

:::note info
テスト駆動開発の名著であるKent Black氏の「[テスト駆動開発](https://www.amazon.co.jp/%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA-Kent-Beck/dp/4274217884/ref=sr_1_1?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=2VB6XPURPMNKS&keywords=%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA&qid=1677907438&sprefix=%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA%2Caps%2C206&sr=8-1 "テスト駆動開発")」をまだ読んでいません。
あくまでもネットベースの知識で実践しています。
:::

# テスト駆動開発とは
以下全て[テスト駆動開発（TDD）とは？目的や種類、メリット・デメリットまで徹底解説](https://service.shiftinc.jp/column/4654/ "TDD")より引用
>テスト駆動開発（Test-Driven Development）の略称としてTDDと呼ばれています。
簡単にご説明すると、テストファーストな開発手法の1つで、従来行われてきた開発プロセス≒“ウォーターフォール型”に代表されるような「設計->実装->テスト」のような直線を描くようなスタイルに対し、「テスト->実装->リファクタリング」を何回も繰り返してプロダクトを成長させていくような開発手法です。

### メリット
>機能を実装するために必要な最低限のコードを作成していきますので、実装がシンプルになります。シンプルであるがゆえに不具合が少なくなります。また、QA段階で確認していたような部分なども開発段階で不具合を多数発見し修正していくことができるため、結果的に品質が上がっていきます。

### デメリット
>仕様が変わることでテストコードを書き換える必要が出てくるため、メンテナンスが大変です。例えば、仕様変更に伴ってテストケースを考えるときに、考慮する内容が増えたり、テストコード自体が増えたりもします。

## 開発の進め方

```mermaid
flowchart LR
RED --> GREEN
GREEN --> REFACTORING
REFACTORING --> RED

style RED color:white,  fill:red, stroke:red
style GREEN color:white,  fill:green, stroke:green
style REFACTORING color:white,  fill:blue, stroke:blue
```

>著名な書籍「テスト駆動開発入門」（Kent Beck 著 | 和田 卓人 訳）では上記のように記載がありますが、わかりやすくご説明すると以下になります。
>>1.レッド：まずは仕様に対して失敗する（エラーになる）テストコードを書きます。
2.グリーン：1.をもとに成功する（パスする）コードを書きます。
3.リファクタリング：できたものに対して余分なものをそぎ落としたりし、きれいに整えていきます。

# Todoアプリ作成
実際にReact + TypeScriptでプロジェクトを作成しテスト駆動開発をやってみる。

### 仕様
非常に簡易的なものとなっています。
* Todoを表示
* Todoを登録
* Todoを削除

### テストケース
* 「Todo List」とタイトルが表示されているか
* Todoが登録できるか
* Todoを削除できるか

## プロジェクト作成
```zsh
npx create-react-app counter-app-tdd --template typescript
```

### App.test.tsx削除
今回はAppコンポーネントはテスト対象外とするので以下コマンドを実行
```zsh
rm App.test.tsx
```

### Tailwind CSSとdaisyUIを導入
以下をご参照ください
[ReactにdaisyUIを導入するまで](https://qiita.com/mavengr/items/455c56ece37d03cfa06d "ReactにdaisyUIを導入するまで")

## テストを始める前に
テスト対象の関数コンポーネントのみを先に作成しておく

```src/components/List.tsx
import React from 'react';

const List: React.FC = () => {
  return <div>List</div>
};

export default List;
```

## テストコード作成
```src/__test__/List.test.tsx
import List from '../components/List';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Listコンポーネントのテスト', () => {
  test('タイトルが表示されているか', () => {
    render(<List />);
    const h1El = screen.getByRole('heading', { name: 'Todo List' });
    expect(h1El).toBeInTheDocument();
  });

  test('Todoを入力して登録できるか', async () => {
    const user = userEvent.setup();
    render(<List />);
    const inputEl = screen.getByRole('textbox');
    const btnEl = screen.getByRole('button', { name: 'ADD' });
    await user.type(inputEl, 'Hello');
    await user.click(btnEl);
    expect(screen.getByText('Hello')).toBeInTheDocument;
  });

  test('Todoを削除できるか', async () => {
    const user = userEvent.setup();
    render(<List />);
    const inputEl = screen.getByRole('textbox');
    const btnEl = screen.getByRole('button', { name: 'ADD' });
    await user.type(inputEl, 'Hello');
    await user.click(btnEl);
    expect(screen.getByRole('button', { name: 'DELETE' }));
    expect(user.click(screen.getByRole('button', { name: 'DELETE' })));
    expect(screen.getByText('Hello')).not.toBeInTheDocument;
  });
});
```

### テストコードを実行
実装していない状態でテストコードを走らせてみる
```zsh
npm test
```

![スクリーンショット 2023-03-11 18.03.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/9b101604-968f-789a-2bcd-677aad5e52d6.png)

当然失敗する。

## List.tsxの作成
```src/components/List.tsx
import React, { useState } from 'react';

type Todo = {
  inputValue: string;
  id: number;
  checked: boolean;
};

const List: React.FC = () => {
  const [inputValue, setInputValue] = useState('');
  const [todos, setTodos] = useState<Todo[]>([]);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setInputValue(e.target.value);
  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    //ADDボタンを押した際のリロード回避
    e.preventDefault();

    //新しいTodoを作成
    const newTodo: Todo = {
      inputValue: inputValue,
      id: todos.length,
      checked: false,
    };
    setTodos([newTodo, ...todos]);
    setInputValue('');
  };

  const handleDelete = (id: number) => {
    const newTodos = todos.filter((todo) => todo.id !== id);
    setTodos(newTodos);
  };

  return (
    <>
      <h1 className="mt-4 mb-4 text-center text-2xl font-bold text-gray-800 md:mb-6 lg:text-3xl">
        Todo List
      </h1>
      <form
        onSubmit={(e) => {
          handleSubmit(e);
        }}
      >
        <input
          type="text"
          placeholder="Type your Todo"
          onChange={(e) => {
            handleChange(e);
          }}
          className="input input-bordered input-primary w-full max-w-xs"
        />
        <button type="submit" className="btn btn-primary ml-5">
          ADD
        </button>
      </form>
      <ul className="mt-5">
        {todos.map((todo) => (
          <li key={todo.id}>
            {todo.inputValue}
            <button
              onClick={() => handleDelete(todo.id)}
              className="m-4 btn btn-secondary"
            >
              DELETE
            </button>
          </li>
        ))}
      </ul>
    </>
  );
};

export default List;
```

## 実装後のテスト
テストを実施し仕様通り作れているか確認
```zsh
npm test
```
テストに合格したので仕様通り作られている
![スクリーンショット 2023-03-11 19.23.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/fb903bc7-d775-8a9a-4766-3540d31b4eb6.png)

# 完成系
![画面収録-2023-03-11-19.25.38.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2860566/68532537-f103-e0ad-c525-44fbe8544a28.gif)

# 最後に
今回作ったTodoとテストコードは完璧なものではありません。
ただ実践していく中で事前にしっかり要件定義を固める、テストケースをしっかり考えるという事を行わないとTDDは難しいと感じました。