Jest で open handle エラーが出た時の対処法

## はじめに
`React` アプリケーションで `Supabase` を使用したテストを実行すると、以下のようなエラーが発生しました。
このエラーの解決方法をまとめます。

```terminal
Jest has detected the following 1 open handle potentially keeping Jest from exiting:

  ●  MESSAGEPORT

       9 | }
      10 |
    > 11 | export const supabase = createClient(supabaseUrl, supabaseKey);
         |                                     ^
      12 |
```

## 発端
テスト実行時に`Supabase`へ接続していたため、テスト終了後も接続が切れず、`Jest`が「まだ開いている通信がある」と警告を出していました。

:::note info
`open handle`（オープンハンドル）とは？
⇨まだ開いたままのリソース（通信・接続・タイマーなど）がある状態のこと
:::

## 解決策

### Supabaseクライアントのモック化
この問題を解決するには、テスト終了後に`Supabase`との接続を切る必要があります。
しかし、今回はテスト実行時に`Supabase`接続は行わず、`Supabase`クライアントをモック化することで解決しました。


```tsx:App.test.tsx
// Supabaseクライアントのモック化
jest.mock('@supabase/supabase-js', () => ({
  createClient: jest.fn(() => ({
    from: jest.fn(() => ({
      select: jest.fn().mockReturnThis(),
      eq: jest.fn().mockReturnThis(),
      single: jest.fn().mockResolvedValue({ data: null, error: null }),
      insert: jest.fn().mockResolvedValue({ data: null, error: null }),
      update: jest.fn().mockResolvedValue({ data: null, error: null }),
      delete: jest.fn().mockResolvedValue({ data: null, error: null }),
    })),
  })),
}));

// アプリケーション固有の関数のモック化
jest.mock('../lib/user', () => ({
  getUserById: jest.fn().mockResolvedValue({ user_id: 'testuser' }),
  // 他の関数...
}));
```

このモックは `@supabase/supabase-js` ライブラリの `createClient` 関数自体をモック化しています。
実際の`Supabase`クライアントが初期化されないため、オープンハンドルは作成されません。

- - -
<details><summary>コード全体</summary>

```tsx:App.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { App } from '../App';
import '@testing-library/jest-dom';
import { ChakraProvider } from '@chakra-ui/react';
import { getUserSkillById } from '../lib/userSkill';

/**
 * モックの作成
 */
// Supabaseクライアントのモック化
jest.mock('@supabase/supabase-js', () => ({
  createClient: jest.fn(() => ({
    from: jest.fn(() => ({
      select: jest.fn().mockReturnThis(),
      eq: jest.fn().mockReturnThis(),
      single: jest.fn().mockResolvedValue({ data: null, error: null }),
      insert: jest.fn().mockResolvedValue({ data: null, error: null }),
      update: jest.fn().mockResolvedValue({ data: null, error: null }),
      delete: jest.fn().mockResolvedValue({ data: null, error: null }),
    })),
  })),
}));

// アプリケーション固有の関数のモック化
jest.mock('../lib/userSkill', () => ({
  getUserSkillById: jest.fn(),
  getUserSkillForEdit: jest.fn(),
}));

jest.mock('../lib/user', () => ({
  insertUser: jest.fn().mockResolvedValue(true),
  updateUser: jest.fn().mockResolvedValue(true),
}));

// テストケース
describe('UserCard Component', () => {
  // テスト前後の処理
  beforeEach(() => jest.clearAllMocks());
  afterEach(() => jest.resetAllMocks());

  it('名刺カードの表示', async () => {
    // テストの実装
    // ...
  });
});
```
</details>

## 解決策のメリット
- **根本的な解決** : `createClient` 自体をモック化するため、実際の`Supabase`接続が作られない
- **シンプル** : 必要なメソッドだけモック化している
- **安定性** : 外部依存がないので、テストが安定する

## 最後に
`Supabase`を使った`React`アプリのテストで「オープンハンドル」問題が発生した場合は、`Supabase`クライアント自体をモック化することで解決することができました。

## 参考

https://jestjs.io/ja/docs/mock-functions

https://stackoverflow.com/questions/68437734/jest-has-detected-the-following-1-open-handle-potentially-keeping-jest-from-exit