はじめに
フロントエンドのテスト入門ということで、今回はTodoアプリを題材にフロントエンドのテストを書いていきます。
Jestとは
Jest はシンプルさを重視した、快適な JavaScript テスティングフレームワークです。
設定も容易で、各種テスト用APIも豊富なテストフレームワークの一つです。
testing-libraryとは
テストを実行したいコンポーネントの描写やクリックイベントの実行、描写した内容からの要素の取得に活用できるライブラリです。
環境構築
create-react-app
で構築さえすれば、一通り必要なパッケージはインストールされています。
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@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"
}
対象コンポーネント
Todo.js
import { useState } from 'react';
import List from "./List"
import Form from "./Form"
const Todo = () =>{
const todoList = [
{ id: 1,
content: "店を予約する"
},
{ id: 2,
content: "筋トレ"
},
{ id: 3,
content: "プログラミング"
},
]
const [ todos, setTodos ] = useState(todoList)
const deleteTodo = (id) => {
const newTodos = todos.filter((todo) => {
return todo.id !== id;
})
setTodos(newTodos);
}
const createTodo = (todo) => {
setTodos([...todos, todo])
}
return (
<div>
<List todos={todos} deleteTodo={deleteTodo}/>
<Form createTodo={createTodo}/>
</div>
)
}
export default Todo;
List.js
const List = ({todos ,deleteTodo}) =>{
const complete = (id) => {
deleteTodo(id)
}
return (
<div>
{ todos.map( todo => {
return (
<div key={todo.id}>
<button onClick={() => complete(todo.id)}>完了</button>
<button>{todo.id}</button>
<button>{todo.content}</button>
</div>
)
})}
</div>
)
}
export default List;
Form.js
import { useState } from 'react';
const Form = ({createTodo}) =>{
const [ enteredTodo, setEnteredTodo ] = useState("");
const addTodo = (e) => {
e.preventDefault()
const newTodo = {
id: Math.floor(Math.random() * 1e5),
content: enteredTodo,
};
createTodo(newTodo);
setEnteredTodo("");
}
return (
<div>
<form onSubmit={addTodo}>
<input
type="text"
value={enteredTodo}
onChange={(e) =>{
setEnteredTodo(e.target.value)
}}/>
<button>追加</button>
</form>
</div>
);
}
export default Form;
テスト
App.test.js
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const element = screen.getByText("Todo App");
expect(element).toBeInTheDocument();
});
個人的にはRSpecライクに書けるのでキャッチアップがしやすい印象を受けました。
testの第一引数にテストケース、第二引数にコールバックを渡し、その中で実際のテストを書く流れです。
render
で対象のコンポーネントを呼び出し、screen
で対象の要素を取得、expect().toBeInTheDocument()
で比較することでテストがパスするかどうかを判定しています。
Todo.test.js
import { render, screen, fireEvent } from "@testing-library/react";
import Todo from "./Todo";
test('Todo works correctly', () => {
render(<Todo />);
// ListとFormコンポーネントがレンダリングされていることを確認
expect(screen.getByText("店を予約する")).toBeInTheDocument();
expect(screen.getByText("筋トレ")).toBeInTheDocument();
expect(screen.getByText("プログラミング")).toBeInTheDocument();
expect(screen.getByText("追加")).toBeInTheDocument();
// 新しいtodoを追加
fireEvent.change(screen.getByRole('textbox'), { target: { value: '新しいタスク' } });
fireEvent.click(screen.getByText('追加'));
// 新しいtodoが追加されたことを確認
expect(screen.getByText('新しいタスク')).toBeInTheDocument();
// todoを削除
fireEvent.click(screen.getAllByText('完了')[0]);
// todoが削除されたことを確認
expect(screen.queryByText('店を予約する')).not.toBeInTheDocument();
});
Form.test.js
import { render, fireEvent, screen } from '@testing-library/react';
import Form from './Form';
test('Form submits the input value', () => {
const createTodo = jest.fn();
render(<Form createTodo={createTodo} />);
// ユーザーがテキストを入力するシミュレーション
fireEvent.change(screen.getByRole('textbox'), { target: { value: '新しいタスク' } });
// ユーザーがフォームを送信するシミュレーション
fireEvent.click(screen.getByText('追加'));
// createTodoが正しい引数で呼び出されたことを確認
expect(createTodo).toHaveBeenCalledWith({ id: expect.any(Number), content: '新しいタスク' });
});
List.test.js
import { render,screen } from "@testing-library/react"
import List from "./List"
test('exist button tag', () => {
const todoList = [
{ id: 1,
content: "店を予約する"
},
{ id: 2,
content: "筋トレ"
},
{ id: 3,
content: "プログラミング"
},
]
render(<List todos={todoList}/>)
const buttonEl = screen.getByText("筋トレ")
expect(buttonEl).toBeInTheDocument();
})
test('List renders correctly', () => {
const todoList = [
{ id: 1, content: "店を予約する" },
{ id: 2, content: "筋トレ" },
{ id: 3, content: "プログラミング" },
];
render(<List todos={todoList} deleteTodo={() => {}} />);
// 各todoが正しくレンダリングされていることを確認
todoList.forEach(todo => {
expect(screen.getByText(todo.content)).toBeInTheDocument();
});
// 各todoに対応する「完了」ボタンが存在することを確認
const buttons = screen.getAllByText("完了");
expect(buttons).toHaveLength(todoList.length);
});
最後に
テスト手法も学んでいきながら、効率よくテストをかけるようにしていきたいです!
参考