Bunのtestを触ってみる

この記事は[Ateam LifeDesign Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ateam-life-design)の11日目記事です

# はじめに
5周遅れぐらいですがアドカレの機会を使ってBunのtest周りを触ります

多いであろうNextJS+jestの構成からBunでのtestに移行しある程度Passするところまでやります

# セットアップ
## NextJS + jestの準備
まずは https://nextjs.org/docs/app/building-your-application/testing/jest を参考にNextJS + jestの環境を用意します

```zsh:
# PJ作成
npx create-next-app@latest --example with-jest with-jest-app

# 起動テスト
cd with-jest/
npm run dev

# テスト実行
npm test

# コンソールエラーが出て見づらいのでパッケージをアップデート
npm outdated
npm uninstall @testing-library/react
npm install -D @testing-library/react

# 記録用にテスト実行
npm test
```

```:テスト実行結果
 PASS  app/utils/add.test.ts
 PASS  app/page.test.tsx
 PASS  app/blog/[slug]/page.test.tsx
 PASS  __tests__/snapshot.tsx
 PASS  app/counter.test.tsx
 PASS  __tests__/index.test.tsx

Test Suites: 6 passed, 6 total
Tests:       6 passed, 6 total
Snapshots:   1 passed, 1 total
Time:        3.243 s
Ran all test suites.
```

## bun testに移行する
https://bun.sh/  をinstallします

```bash:
curl -fsSL https://bun.sh/install | bash
exec /bin/zsh 
bun --help

# テスト実行
bun test
```

この4行でテスト実行可能なところまでいけます
実行すると以下のエラー

```:bun testの結果
bun test v1.1.38 (bf2f153f)

app/page.test.tsx:
234 |     throw error;
235 |   }
236 |   if (!baseElement) {
237 |     // default to document.body instead of documentElement to avoid output of potentially-large
238 |     // head elements (such as JSS style blocks) in debug output
239 |     baseElement = document.body;
                        ^
ReferenceError: Can't find variable: document
      at render (/workspace/with-jest-app/node_modules/@testing-library/react/dist/pure.js:239:19)
      at /workspace/with-jest-app/app/page.test.tsx:8:3
✗ App Router: Works with Server Components [3.47ms]

app/counter.test.tsx:
234 |     throw error;
235 |   }
236 |   if (!baseElement) {
237 |     // default to document.body instead of documentElement to avoid output of potentially-large
238 |     // head elements (such as JSS style blocks) in debug output
239 |     baseElement = document.body;
                        ^
ReferenceError: Can't find variable: document
      at render (/workspace/with-jest-app/node_modules/@testing-library/react/dist/pure.js:239:19)
      at /workspace/with-jest-app/app/counter.test.tsx:8:3
✗ App Router: Works with Client Components (React State) [0.22ms]

__tests__/index.test.tsx:
234 |     throw error;
235 |   }
236 |   if (!baseElement) {
237 |     // default to document.body instead of documentElement to avoid output of potentially-large
238 |     // head elements (such as JSS style blocks) in debug output
239 |     baseElement = document.body;
                        ^
ReferenceError: Can't find variable: document
      at render (/workspace/with-jest-app/node_modules/@testing-library/react/dist/pure.js:239:19)
      at /workspace/with-jest-app/__tests__/index.test.tsx:9:5
✗ Home > renders a heading [0.75ms]

app/utils/add.test.ts:

# Unhandled error between tests
-------------------------------
1 | throw new Error(
          ^
error: This module cannot be imported from a Client Component module. It should only be used from a Server Component.
      at /workspace/with-jest-app/node_modules/server-only/index.js:1:7
-------------------------------


app/blog/[slug]/page.test.tsx:

# Unhandled error between tests
-------------------------------
 7 | import 'lodash/isEqualWith.js';
 8 | import 'lodash/uniq.js';
 9 | import 'css.escape';
10 | import 'aria-query';
11 | 
12 | expect.extend(extensions);
     ^
ReferenceError: Can't find variable: expect
      at /workspace/with-jest-app/node_modules/@testing-library/jest-dom/dist/index.mjs:12:1
-------------------------------


 0 pass
 5 fail
 2 errors
Ran 5 tests across 5 files. [284.00ms]
```

流石にそのままではダメそうですね

エラー内容と、bunが[jestからの移行ガイド](https://bun.sh/guides/test/migrate-from-jest)を出しているので見ながら直していきます

### ReferenceError: Can't find variable: document
DOMにアクセスする場合は`happy-dom`の導入が必要みたいなので入れます

https://bun.sh/guides/test/happy-dom

```zsh:
bun add -d @happy-dom/global-registrator
touch happydom.ts bunfig.toml
```

```typescript:happydom.ts
import { GlobalRegistrator } from "@happy-dom/global-registrator";
GlobalRegistrator.register();
```

```toml:bunfig.toml
[test]
preload = "./happydom.ts"
```

### toHaveTextContent is undefined
一部のマッチャーは使えないのでbunで使えるものに差し替えます

https://bun.sh/docs/test/writing#matchers

```typescript:app/page.test.tsx
import { render, screen } from "@testing-library/react";
import Page from "./page";

it("App Router: Works with Server Components", () => {
  render(<Page />);
  // toHaveTextContentを変更
  expect(screen.getByRole("heading").textContent).toBe("App Router");
});
```

他のマッチャーも変えていきます

### Found multiple elements with the role "heading"
他テストでrenderされたcomponentがそのまま残り続けているっぽい
~~もっと良い対応がありそうだが納期がやばいので~~getByTestIdに差し替えて対応する

```tsx:app/counter.tsx
"use client";

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h2 data-testid='count'>{count}</h2>
      <button type="button" onClick={() => setCount(count + 1)}>
        +
      </button>
    </>
  );
}
```

```tsx:app/counter.test.tsx
import { fireEvent, render, screen } from "@testing-library/react";
import Counter from "./counter";

it("App Router: Works with Client Components (React State)", () => {
  render(<Counter />);
  expect(screen.getByTestId("count").textContent).toBe("0");
  fireEvent.click(screen.getByRole("button"));
  expect(screen.getByTestId("count").textContent).toBe("1");
});
```

### TypeError: "/vercel.svg" cannot be parsed as a URL.
happy-dom側の問題のようです

https://github.com/oven-sh/bun/issues/13517

対応策を~~納期内に~~見つけられなかったため一旦置きにしています

### It should only be used from a Server Component
`import "server-only";`しているComponentのテストに失敗しています
NextJSの`13.4.20`からは成功するようになったよう

https://zenn.dev/cybozu_frontend/articles/next-rsc-testing

こちらもbun testで対応する方法が納期内に見つけられずでした

# 結果

```zsh:最終的な実行結果
bun test v1.1.38 (bf2f153f)

app/page.test.tsx:
✓ App Router: Works with Server Components [20.93ms]

app/counter.test.tsx:
✓ App Router: Works with Client Components (React State) [12.84ms]

__tests__/index.test.tsx:

app/utils/add.test.ts:

app/blog/[slug]/page.test.tsx:
✓ App Router: Works with dynamic route segments [1.81ms]

 3 pass
 0 fail
 4 expect() calls
Ran 3 tests across 5 files. [360.00ms]
```

以上で3/5のテストの移行を行いました
残り2つについてはすぐには対応策を見つけられなかったので空き時間で調べます…

実行時間についてはテストケースが減ったとはいえ`3.243 s → 360.00ms`と噂通り爆速でした
既にテストがあるプロダクトではエイヤで移行するのは厳しそうですが、
既存プロダクトに新規でテストを追加したり、新しいプロダクトへの採用であれば良さそうです

気になった点としては基本的な書き方は変わらないものの、現状ではまだ必要な変更点が多い点ですね
特にDOM周りの扱いやNextJSだとAppRouter周りに難がありそうでした
純粋なjs等のテストでは良さそうですがNextJSとかと組み合わせていくとまだまだ課題にぶち当たりそうでした

とはいえ早さは圧倒的なので個人的に小さいものを作ったりする時は積極的に採用していこうかと思います
以上で〆させていただきたいと思います
今回の教訓:締め切り駆動執筆はやめましょう