5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Jest】ReactのTodoアプリを題材にフロンドエンドテスト入門

Last updated at Posted at 2023-12-09

はじめに

フロントエンドのテスト入門ということで、今回はTodoアプリを題材にフロントエンドのテストを書いていきます。

Image from Gyazo

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);
});

最後に

テスト手法も学んでいきながら、効率よくテストをかけるようにしていきたいです!

参考

5
4
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
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?