はじめに
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を導入するまで
テストを始める前に
テスト対象の関数コンポーネントのみを先に作成しておく
import React from 'react';
const List: React.FC = () => {
return <div>List</div>
};
export default List;
テストコード作成
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
当然失敗する。
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
完成系
最後に
今回作ったTodoとテストコードは完璧なものではありません。
ただ実践していく中で事前にしっかり要件定義を固める、テストケースをしっかり考えるという事を行わないとTDDは難しいと感じました。