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

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

[![Image from Gyazo](https://i.gyazo.com/d6dfea27ed9659544a66e94cce116464.gif)](https://gyazo.com/d6dfea27ed9659544a66e94cce116464)

# Jestとは
> Jest はシンプルさを重視した、快適な JavaScript テスティングフレームワークです。

設定も容易で、各種テスト用APIも豊富なテストフレームワークの一つです。

https://jestjs.io/ja/

# testing-libraryとは

テストを実行したいコンポーネントの描写やクリックイベントの実行、描写した内容からの要素の取得に活用できるライブラリです。

https://testing-library.com/


# 環境構築
`create-react-app`で構築さえすれば、一通り必要なパッケージはインストールされています。

```json
  "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"
  }
```

# 対象コンポーネント 

```jsx: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;
```
```jsx: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;
```

```jsx: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;
```

# テスト
```jsx: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()`で比較することでテストがパスするかどうかを判定しています。

```jsx: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();
});
```

```jsx: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: '新しいタスク' });
});

```

```jsx: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);
});

```

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

# 参考

https://qiita.com/ossan-engineer/items/4757d7457fafd44d2d2f

https://zenn.dev/iamtillmans/articles/171f41fbd03c89