LoginSignup
4
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-11

はじめに

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

テスト駆動開発の名著であるKent Black氏の「テスト駆動開発」をまだ読んでいません。
あくまでもネットベースの知識で実践しています。

テスト駆動開発とは

以下全てテスト駆動開発(TDD)とは?目的や種類、メリット・デメリットまで徹底解説より引用

テスト駆動開発(Test-Driven Development)の略称としてTDDと呼ばれています。
簡単にご説明すると、テストファーストな開発手法の1つで、従来行われてきた開発プロセス≒“ウォーターフォール型”に代表されるような「設計->実装->テスト」のような直線を描くようなスタイルに対し、「テスト->実装->リファクタリング」を何回も繰り返してプロダクトを成長させていくような開発手法です。

メリット

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

デメリット

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

開発の進め方

著名な書籍「テスト駆動開発入門」(Kent Beck 著 | 和田 卓人 訳)では上記のように記載がありますが、わかりやすくご説明すると以下になります。

1.レッド:まずは仕様に対して失敗する(エラーになる)テストコードを書きます。
2.グリーン:1.をもとに成功する(パスする)コードを書きます。
3.リファクタリング:できたものに対して余分なものをそぎ落としたりし、きれいに整えていきます。

Todoアプリ作成

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

仕様

非常に簡易的なものとなっています。

  • Todoを表示
  • Todoを登録
  • Todoを削除

テストケース

  • 「Todo List」とタイトルが表示されているか
  • Todoが登録できるか
  • Todoを削除できるか

プロジェクト作成

npx create-react-app counter-app-tdd --template typescript

App.test.tsx削除

今回はAppコンポーネントはテスト対象外とするので以下コマンドを実行

rm App.test.tsx

Tailwind CSSとdaisyUIを導入

以下をご参照ください
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;
  });
});

テストコードを実行

実装していない状態でテストコードを走らせてみる

npm test

スクリーンショット 2023-03-11 18.03.46.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;

実装後のテスト

テストを実施し仕様通り作れているか確認

npm test

テストに合格したので仕様通り作られている
スクリーンショット 2023-03-11 19.23.29.png

完成系

画面収録-2023-03-11-19.25.38.gif

最後に

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

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3