【React×Jest】Reactのテスト時に非同期処理でUnhandledPromiseRejectionエラーが出た際の対処法

## はじめに
ReactのテストコードをJestとモック関数を使って書いていた際、**UnhandledPromiseRejection**というエラーが発生してテストコードの結果自体が表示されない現象が発生しました。

当記事では`UnhandledPromiseRejection`エラーが発生しうる原因と実際に行った対処法について書いていきます。

## 問題発生時のコード
### テスト実行用コード
```react:test.spec.tsx
jest.mock("../utils/supabase-function", () => {
  return {
    getUserData: jest.fn(),
  };
});

describe("名刺カードページのテスト", () => {
  test("文言が存在している", async () => {
    const usingData = new UserDataRecord(
      {/* ここには引数として渡すデータを置く */}
    );
    (getUserData as jest.Mock).mockRejectedValue(usingData);
    
    render(
      <BrowserRouter>
        <IdPage />
      </BrowserRouter>,
    );
    expect(screen.getByText("Now loading...")).toBeInTheDocument();
  });
});
```
※Reject自体の原因としては上記で`mockResolvedValue`とすべきところを`mockRejectedValue`としてReject処理をかけていたことが原因。

`UnhandledPromiseRejection`エラーが発生した原因は下記。

### 呼び出し元のコンポーネント
```react:IdPage.tsx
{/* 今回のテスト処理に直接関係がある部分のみ抜粋 */}
useEffect(() => {
    const userDataGetting = async () => {
      const gettingUserData = await getUserData(id);
      setUserData(gettingUserData);
      setLoading(false);
    };
    userDataGetting();
}, []);
```

### supabaseとの連携部分
```react:supabase-function.tsx
export const getUserData = async (value?: number | string) => {
  const response = await supabase.from("users").select("*")
    .eq("user_id", value);

  if (response.error) {
    throw new Error(response.error.message);
  }

  const newData = response.data.map((value) => {
    return UserDataRecord.newUserDataRecord(
      {/* 引数としてデータ型に応じたデータを返す */}
    );
  });

  return newData;
};
```

## 発生したエラー
上記の状態でJestのテストを実行すると以下のようなエラーが発生し、SuccessにもFailにもならずに処理自体が停止した。
```
<!-- 一部改行を加えています -->
node:internal/process/promises:389
      new UnhandledPromiseRejection(reason);
      ^

UnhandledPromiseRejection: This error originated either by throwing 
inside of an async function without a catch block, 
or by rejecting a promise which was not handled with 
.catch(). The promise rejected with the reason "[object Object]".
    at throwUnhandledRejectionsMode (node:internal/process/promises:389:7)
    at processPromiseRejections (node:internal/process/promises:470:17)
    at processTicksAndRejections (node:internal/process/task_queues:96:32) {
  code: 'ERR_UNHANDLED_REJECTION'
}
```

## エラーの内容
上記のエラーの内容を読んでみたところ次のような現象が起きていた。
* **catchブロックがない非同期関数の内側で(エラーが)スローされた**
* **catch()で処理されていないPromiseが拒否(Reject)された**
* **このPromiseは[object Object]という理由でrejectされている**

つまり上記の問題はコンポーネント内でエラーが発生した際に、明示的にエラー時の処理を書いていないことで起きたと推測された。

元のコンポーネント側ではsupabaseの連携用関数の方にエラーをthrowさせており、`useEffect`内で処理がRejectされることを想定しないコードとなっていた。

## 対策
### ①テストコードに値を渡す際は`mockResolvedValue`もしくは`mockResolvedValueOnce`で渡す
そもそも当然と言えば当然なのだが、`mockRejectedValue`だと常に処理結果をRejectするためのテスト値を渡してしまっている。

意図的にReject時の挙動を確かめたいといった特別な事情がない限り、**処理の結果を正常にテストするためには`mockResolvedValue`もしくは`mockResolvedValueOnce`を使う**。

### ②コンポーネント側でエラー時の処理を定義するようなcatchブロックを作る
useEffectの内側にエラー(Reject)時の記述を書いていなかったことも原因なので、**catchブロックの内側に明示的にエラー時の処理を記述**する。
```react:IdPage.tsx
  useEffect(() => {
    const userDataGetting = async () => {
     {/* try...catch...構文を追記 */}
      try {
        const gettingUserData = await getUserData(id);
        setUserData(gettingUserData);
        setLoading(false);
      } catch (error) {
        console.error("IDが一致しません", error);
      }
    };

    userDataGetting();
  }, []);
```
上記のように修正することで該当のエラーは解消された。

## 最後に
モック関数を用いたエラーは多くの場合で「値を正常に返す」ことを想定して行われるはずです。
万が一設定を間違えて`UnhandledPromiseRejection`エラーを発生させてしまった場合にはこの記事を参考にしてみてください。


## 参考資料
https://archive.jestjs.io/docs/ja/22.x/mock-function-api

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/try...catch

https://zenn.dev/uhyo/articles/unhandled-rejection-understanding

https://zenn.dev/dhik/articles/50246e1dbf86ae