react-testing-libraryで『An update to App inside a test was not wrapped in act(...).』エラー

# はじめに
jest + react-testing-libraryでのテストを実行中、表題のエラーが発生しました。

# 問題
### ボタンをクリックするイベントをテスト実行すると表題のエラーになる。

`An update to App inside a test was not wrapped in act(...).`
日本語訳：`テスト内のAppの更新がact(...)でラップされていなかった。`


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3993509/2aed0cd8-033f-4eaa-ad0b-30e2ee107ae9.png)


**問題が発生したコード**

```tsx:AppComponetnt.spec.tsx
import App from "../App";
import { render, screen } from "@testing-library/react";

describe("title", () => {
  it("登録ができる", async () => {
    render(<App />);
    // 登録ボタンの要素を見つける
    const newRecordButton = await screen.findByTestId("new-record-button");
    // 登録ボタンクリック
    newRecordButton.click();
  });
});

```

# 解決方法

### 1.`act`を使う

https://ja.react.dev/reference/react/act

**ドキュメントより引用**
>UI テストを書く際、レンダー、ユーザイベント、データフェッチなどのタスクは、ユーザインターフェースにおける「操作単位 (unit of interaction)」と捉えることができます。React は act() というヘルパを提供しており、これによりこれらの「操作単位」に関連するすべての更新が処理されて DOM に適用された後に、アサーションを行えるようになります。

* ボタンをクリックすると、Reactは状態の更新を検知し、再レンダリングが行われます
* act() を使わない場合、再レンダリングを行っている際に次のプロセスに進もうとし、エラーになってしまいます
* act() を使うことで、操作単位（今回の場合は、ボタンをクリックする操作）を行った後、次のプロセスに進むことができます

```diff_tsx:AppComponetnt.spec.tsx
import App from "../App";
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen, act } from "@testing-library/react";

describe("title", () => {
  it("登録ができる", async () => {
    render(<App />);
    // 登録ボタンの要素を見つける
    const newRecordButton = await screen.findByTestId("new-record-button");
    // 登録ボタンクリック
-    newRecordButton.click();
+    act(() => {
+      newRecordButton.click();
+    });
  });
});
```

### 2.`fireEvent`を使う

https://testing-library.com/docs/react-testing-library/cheatsheet/#events

fireEventについてドキュメントを確認すると以下の記述がありました
>act wrapper around react act; React Testing Library wraps render and fireEvent in a call to act already so most cases should not require using it manually
>>日本語訳：リアクト・テスティング・ライブラリは、renderとfireEventをすでにactの呼び出しでラップしているので、ほとんどの場合、手動で使う必要はないはずだ。

fireEventを使用する場合、内部でactを使用しているため、actを使う必要がないと理解しました。
fireEventを使用した場合もエラーとならずテスト実行をすることができました。

```diff_tsx:AppComponetnt.spec.tsx
import App from "../App";
import { fireEvent, render, screen, act } from "@testing-library/react";

describe("title", () => {
  it("登録ができる", async () => {
    render(<App />);
    // 登録ボタンの要素を見つける
    const newRecordButton = await screen.findByTestId("new-record-button");
    // 登録ボタンクリック
-    act(() => {
-     newRecordButton.click();
      fireEvent.click(newRecordButton);
-    });
  });
});
```

### 3.`userEvent`を使う

https://testing-library.com/docs/user-event/intro/

>user-event allows you to describe a user interaction instead of a concrete event. It adds visibility and interactability checks along the way and manipulates the DOM just like a user interaction in the browser would. It factors in that the browser e.g. wouldn't let a user click a hidden element or type in a disabled text box.
This is why you should use user-event to test interaction with your components.
>>日本語訳：user-eventは、具体的なイベントの代わりにユーザーとのインタラクションを記述することができます。その過程で可視性とインタラクタビリティのチェックを追加し、ブラウザでのユーザーインタラクションのようにDOMを操作します。ブラウザは、例えば、ユーザーが非表示の要素をクリックしたり、無効化されたテキストボックスに入力したりすることを許しません。
これが、コンポーネントとのインタラクションをテストするためにuser-eventを使うべき理由です。

fireEventよりもよりユーザーの操作に近いテストを実現できると理解しました。
```diff_tsx:AppComponetnt.spec.tsx
import App from "../App";
import { fireEvent, render, screen, act } from "@testing-library/react";

describe("title", () => {
  it("登録ができる", async () => {
+   const user = userEvent.setup();
    render(<App />);
    // 登録ボタンの要素を見つける
    const newRecordButton = await screen.findByTestId("new-record-button");
    // 登録ボタンクリック
-   fireEvent.click(newRecordButton);
+   user.click(newRecordButton);
  });
});
```


# おわりに
actでラップしてくださいのエラーから、芋づる式で様々な解消方法が判明したことは何気に嬉しいです。
ドキュメントによると実際のユーザー操作に近い`userEvent`を使用することを推奨していました。


# 参考
https://ja.react.dev/reference/react/act

https://testing-library.com/docs/react-testing-library/cheatsheet/#events

https://testing-library.com/docs/user-event/intro/