BunのテストランナーとJestの速さの比較をしてみた

# はじめに
2023/9/8に Javascript ランタイムの一種である Bun のv1.0.0がリリースされました。(2023/12/18現在では最新v1.0.18)

[Bun公式](https://bun.sh/)によると実行速度の速さをかなり推しているようで, 1 秒あたりの HTTP リクエスト (Linux x64)の速さは Node.js (v20.5.0)の約4倍を誇るようです。

また2022 年 8 月に700 万ドルの資金調達を獲得し、大きな注目を集めていました。

そこで今回は Bun にデフォルトで付属しているテストランナーを使って、node.js に比べてどのくらいテストの実行速度が変わるか試してみようと思います。

# Bunのテストランナーについて
Bun には、Jest 互換の高速な組み込みテストランナーが付属しています。テストは Bun ランタイムで実行され、次の機能をサポートしているようです。

- TypeScript と JSX
- ライフサイクルフック
- snapshotテスト
- UIとDOM テスト
- watch mode オプション(`—-watch`)
- スクリプトのプリロードオプション(`--preload`)

# 今回のテスト対象

今回のテストは友人チームで個人開発で作ったアプリの一画面を対象に行います。
(所属している企業とは一切関係ありません)

https://www.nomipara-introme.com/

実際にテストする画面がこちらです。今回はボタンがselectMenuにボタン要素が存在するかの簡単なテストを実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/586935/6f98c8ad-1608-2ad7-bc3a-c9de1aad07ef.png)

使用している主なライブラリバージョンは以下の通りです。

- React 18.2.0
- Next.js 12.2
- Redux 4.2.1
- MUI 5.11.4
- jest 29.7.0
- bun 1.0.18
- testing-library/react 14.1.2
- testing-library/jest-dom 6.1.5

# Jestを用いたテストコード

まずはJestテストファイルです。

``` react
import '@testing-library/jest-dom';
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { ThemeProvider } from '@mui/material/styles';
import { render, waitFor, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import theme from '../../../libs/theme';
import { BasicInfoForm } from './basicInfoForm.container';

const mockAppDispatch = jest.fn();
const mockAppSelecter = jest.fn();
const mockUpdateCurrentUsername = jest.fn();

jest.mock('react-redux', () => ({
  useDispatch: jest.fn(),
  useSelector: () => mockAppSelecter(),
}));

jest.mock('../../../store', () => ({
  updateCurrentUsername: (...args: any[]) => mockUpdateCurrentUsername(...args),
  useAppDispatch: (...args: any[]) => mockAppDispatch(...args),
  useAppSelector: (...args: any[]) => mockAppSelecter(...args),
}));

describe('BasicInfoFormのテスト', () => {
  beforeEach(() => {
    mockAppDispatch.mockReset();
    mockUpdateCurrentUsername.mockReset();
  });

  test('test', async () => {
    const setCurrentStepMock = jest.fn();

    const mockquestion = [
      {
        username: '',
        limit: '',
        hobbies: '',
        birthplace: '',
        likes: '',
        goal: '',
        mode: 'standard',
      },
    ];

    mockAppSelecter.mockReturnValue(mockquestion);

    const { getByTestId, getByText } = render(
      <ThemeProvider theme={theme}>
        <EmotionThemeProvider theme={theme}>
          <BasicInfoForm currentStep={1} setCurrentStep={setCurrentStepMock} />
        </EmotionThemeProvider>
      </ThemeProvider>,
    );

    const sexuialityElem = getByTestId('sexuiality');

    expect(sexuialityElem).toBeInTheDocument();

    const button = within(sexuialityElem).getByRole('button');

    userEvent.click(button);

    await waitFor(() => expect(getByText('男性')).toBeInTheDocument());
  });
});
```

大まかな流れとしては

1. コンポーネントの中で使っている redux の dispatch関数などをmock
2. すべての処理の前にmock関数をリセット
3. テスト対象のコンポーネントをレンダリング(EmotionとMUIを用いているため, それらのProviderでラップする必要がある)
4. 要素が存在するかのテスト

となっています。

このテストを実行した結果は以下です。

``` bash
PASS  src/components/organisms/basicInfoForm/basicInfoForm.test.tsx (29.784 s)
  BasicInfoFormのテスト
    ✓ test (1000 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        30.971 s
```

では次にBunのテストランナーで実行するテストコードを見ていきます。

# Bunテストランナー用コード

以下にテストコードを示します

``` react
import { ThemeProvider as EmotionThemeProvider } from '@emotion/react';
import { GlobalRegistrator } from '@happy-dom/global-registrator';
import { ThemeProvider } from '@mui/material/styles';
import { render, within } from '@testing-library/react';
import { test, expect, jest, mock, describe, beforeEach } from 'bun:test';
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';
import theme from '../../../libs/theme';

import store, { persistor } from '../../../store';
import { BasicInfoForm } from './basicInfoForm.container';

GlobalRegistrator.register();

describe('BasicInfoFormのテスト', () => {

  test('test', async () => {
    const setCurrentStepMock = jest.fn();

    const mockquestion = [
      {
        username: '',
        limit: '',
        hobbies: '',
        birthplace: '',
        likes: '',
        goal: '',
        mode: 'standard',
      },
    ];

    const { getByTestId, getByText } = render(
      <ReduxProvider store={store}>
        <PersistGate loading={null} persistor={persistor}>
          <ThemeProvider theme={theme}>
            <EmotionThemeProvider theme={theme}>
              <BasicInfoForm currentStep={1} setCurrentStep={setCurrentStepMock} />
            </EmotionThemeProvider>
          </ThemeProvider>
        </PersistGate>
      </ReduxProvider>
    );

    const sexuialityElem = getByTestId('sexuiality');

    const button = within(sexuialityElem).getByRole('button');

    expect(button.tabIndex).toBe(0);
  });
});
```

処理の流れはJestコードとほぼ同じです。

このテストを実行した結果は以下です。

``` bash
✓ BasicInfoFormのテスト > test [364.29ms]

 1 pass
 0 fail
 1 expect() calls
Ran 1 tests across 1 files. [15.77s]
```

しかしJest互換と上述しましたがいくつかの点でそのままでは実行できないところがありました。

### DOM要素にアクセスするようなテストを実行する場合, 新しくhappy-domを導入する必要がある

DOM にアクセスする必要があるテストは前もって `happy-dom` を導入する必要があるようです。

詳しい導入や実装方法は[コチラ](https://bun.sh/docs/test/dom)

``` react
import { GlobalRegistrator } from "@happy-dom/global-registrator";

GlobalRegistrator.register();
```

### 一部jestのmock関数がそのままでは使えない
jest.fn はそのままjestの記法で使えるようですが、 jest.mock は現在対応しておらず同等機能のmock関数が用意されているようです。

しかし今回、jest のファイルでは Reduxのdispatch関数などを jest.mock を使って mock していたのですが、jest.mock→mockと置き換えただけでは Redux の mock をすることはできなかったため注意が必要そうです。

mockの使い方については[コチラ](https://bun.sh/docs/test/mocks)

### toBeInTheDocument()などの一部マッチャーが使えない
toBeInTheDocumentなどの一部のマッチャーは2023/12/18現在未対応のものが存在するようです。

なので今回はtoBeInTheDocumentはBunテストファイルでは使えなかったため、間接的なアプローチに変更しています。

- jestファイル
```react
const sexuialityElem = getByTestId('sexuiality');

expect(sexuialityElem).toBeInTheDocument();

const button = within(sexuialityElem).getByRole('button');

expect(button).toBeInTheDocument();
```

- Bunテストファイル
```react
    const sexuialityElem = getByTestId('sexuiality');

    const button = within(sexuialityElem).getByRole('button');

    expect(button.tabIndex).toBe(0);
```

[コチラ](https://bun.sh/docs/test/writing#matchers)から対応表の一覧が公開されています

## 比較結果
実行結果は以下の通りです。

|      | 実行時間(s) | 
| ---- | ----------- | 
| jest | 30.971      | 
| bun  | 15.77       | 


jestに比べてBunの実行時間が約2倍速いことがわかりました。

# まとめ
公式が推している通り、Bunはテストにおける実行時間の速さが優れていることが確認できました。しかし全く同じ処理でも実行結果が変わったり、対応していないマッチャーがあったりとまだまだJestの完全上位互換というわけではない印象でした。とわいえ、速度に関してはとても魅力的なので今後のアップデートを注視していこうと思います。

（個人的にはBunという名前は、BundleのBunと、それらを包み込むという意味から中華まん（Bun）に由来しているそうでそこも推しポイントです。）

# 参考
https://bun.sh/

https://gihyo.jp/article/2023/01/tfen005-bun

https://www.ai-shift.co.jp/techblog/3113

https://zenn.dev/oreo2990/articles/5b56230ba2d7a1

https://tsh.io/blog/bun-benchmark/