こんにちは、ドワンゴ教育事業 Web フロントエンドチームの猪井です。
この記事では babel-jest から @swc/jest に移行することで Jest によるテストが速くなった事例について紹介します。
JavaScript のテストツールは、Vitest のバージョンが 1 を迎えたり Bun が登場したりして、よく使われる Jest 以外にもよさそうな選択肢が増えています。業務の手が少し空いたタイミングでそれらについて調査し実際に試してみたところ、最終的に @swc/jest を使用することで既存のテストを大きく書き換えることなく実行時間を短縮できました。
今回試した JavaScript のテストツール
今回は Vitest、Bun、そして @swc/jest の 3 つを試してみました。
これら以外にも、Node.js 自体に搭載されているテストランナーや、@swc/jest と同じような立ち位置の esbuild-jest などもあります。しかし現状に大きな課題があるわけではないので、既存のコードを大きく書き換えることになる Node.js のテストランナーはそもそも検討せず、また @swc/jest で十分な結果を得られたので esbuild-jest は試しませんでした。
それぞれのざっくりした印象は以下の通りです。
Vitest | Bun | @swc/jest | |
---|---|---|---|
現状と比べた速さ | △? | ◎ | ○ |
移行作業 | マイグレーションガイドがあり、多くは機械的に書き換えができそう1 | 完全ではないものの jest.spyOn などがそのまま動くので書き換える部分は少なそう |
設定を書き換えるだけ |
よさそうなところ | ESM が普通に動く | 圧倒的に速い | 既存のコードをほぼ書き換えずに速くなる |
気になるところ | 速くはならなさそう(遅くなる可能性もありそう) | Jest で使えていたもののうち、一部使えない機能がある | 特になし |
Vitest
- https://vitest.dev/
- 1.0 になったのでしばらく大きな変更はなさそう
- ESM が普通に動くのは便利
- Jest でもがんばれば動かせそうではあるが普通に動くというのはありがたい
- 既存のテストファイルで実際に動かしてみたら Jest と同じくらいの速度だった
- Jest からのマイグレーションガイドも用意されていて移行は比較的容易そう
- TextEncoder など、よく使われるものの jsdom に存在しない API がデフォルトで使えるようになっており、customEnvironment を作ったり、setupFiles でなんとかしたりという対応が不要なのは便利そう4
Bun
- https://bun.sh/
- かなり速い
- ちゃんとした計測ではないものの Jest で 640ms かかるテストが Bun では 30ms で終わることがあった
- Jest や Vitest と同じようにテストを書ける
- DOM のテストに jsdom を使っている場合は対応が必要(後述します)
- 自動で jest 互換の
jest
オブジェクトを import してくれてjest.spyOn
などがそのまま動く
- test.each でのテスト名の変数置換が不完全だったり、spyOn のサポートが限定的 だったりする
- テストに関する issue が 2024-02-26 時点で 135 件あり、上記以外にも細かいところでつまずくことが多そう
既存の DOM のテストで jsdom を使っている場合については少し工夫が必要そうでした。現状 Bun のテストランナーには Jest の testEnvironment のような仕組みはなく、document や window といった値は自分でグローバルに定義する必要があります。
happy-dom には @happy-dom/global-registrator というパッケージが用意されており、これを使うことでそれらをグローバルに設定してくれます。一方で jsdom では jsdom の window などをグローバルに定義するのは推奨されていない(↪ Don't stuff jsdom globals onto the Node global)ようで、そうしたパッケージは用意されていません。
結局やることは同じなので @happy-dom/global-registrator と同じように推奨されていないことを承知でグローバルに jsdom の window などを定義してしまうか、Bun への移行のついでに happy-dom に移行する、という選択肢になりそうです。
@swc/jest
- https://www.npmjs.com/package/@swc/jest
- JSX や TypeScript の変換の仕組み(↪ Code Transformation)を変えるだけなので既存のコードを書き換えることなく速度の改善が期待できる
babel-jest から @swc/jest に移行する
Bun のテストランナーはまだ業務で使うには早そう、Vitest については現状に大きな課題があるわけではないので急いで移行する必要はなさそうという印象でした。もちろん今後のことを考えると Vitest で ESM が普通に動くというのは魅力的なものの、速度が遅くなる懸念もあるなら、いまのところは Jest の ESM 対応 を待ってもよさそうです。
そこで今回は babel-jest から @swc/jest に移行することでテストの速度改善を目指すことにしました。
移行は@swc/jest のドキュメントにある通り Jest の設定ファイルの transform
オプションを書き換えるだけです。
module.exports = { transform: { // ここを書き換えるだけ "^.+\\.(t|j)sx?$": "@swc/jest", }, };
基本的にはこれだけでいいのですが、import * as module from "module"
のように import した際の module
のような名前空間オブジェクトに対して、jest.spyOn
を実行している箇所は @swc/jest ではエラーになります。
import * as module from "module"; // ここでエラーになる const m = jest.spyOn(module, "f").mockReturnValue("mocked");
この問題については jest.spyOn で TypeError: Cannot redefine property の記事が大変参考になりました。
私たちのコードでは以下のように素直に jest.mock を使うことで回避しました。
- import * as module from "module"; + import { f } from "module"; + jest.mock("module"); - const m = jest.spyOn(module, "f").mockReturnValue("mocked"); + const m = jest.mocked(f).mockReturnValue("mocked");
この変更によって手元の PC では以下のように 70 秒前後速度が速くなりました。
実行 | babel-jest | @swc/jest |
---|---|---|
1 回目 | 125.57s | 56.14s |
2 回目 | 127.70s | 47.02s |
3 回目 | 131.97s | 55.19s |
Jest はコードの変換結果をキャッシュする5ため、実行時は --no-cache
オプションをつけています。
まとめ
babel-jest から @swc/jest への移行で、手間をあまりかけずにテストが速くなりました。
今回はテストフレームワークとその周辺ツールしか試していませんが、ウェブフロントエンドのテストの高速化では jsdom の代わりに happy-dom を使う手もありそうです。今回見送った Vitest も Bun も活発に開発されているので時間が経ったら諸々の状況が大きく変わっているかもしれません。
We are hiring!
株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。 カジュアル面談も行っています。 お気軽にご連絡ください!
開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。
- 実際に移行しようとするといろいろな問題が起こることがあるらしく、隣のチームでは移行を断念していたようです。↩
-
ローカル実行は Jest と Vitest で体感できるほどの差がありませんでしたが、 CI (GitHub Actions) 上での実行は約 1.5 倍の時間がかかるようになってしまいました。
↪ フロントエンドのテスト基盤を Jest から Vitest に移行した話
↩ - Jest と Vitest の isolate について によると、Vitest で速くならない/遅くなるケースについては isolation まわりの問題が関連しそうです。↩
- Node.js の fetch はウェブのものと微妙に異なるという話を聞いたことがありますが、ドキュメントには "A browser-compatible implementation of the fetch() function." とあり、基本的には大丈夫そうでした。↩
-
↩Jest will cache the result of a transformation