@swc/jestで手間をかけずにテストを早くする

こんにちは、ドワンゴ教育事業 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

Bun

既存の 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(babel-jest) を swc に置き換える
    • ほかにも esbuild を使った esbuild-jest という選択肢もある

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!

株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。 カジュアル面談も行っています。 お気軽にご連絡ください!

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com



  1. 実際に移行しようとするといろいろな問題が起こることがあるらしく、隣のチームでは移行を断念していたようです。
  2. ローカル実行は Jest と Vitest で体感できるほどの差がありませんでしたが、 CI (GitHub Actions) 上での実行は約 1.5 倍の時間がかかるようになってしまいました。

    フロントエンドのテスト基盤を Jest から Vitest に移行した話

  3. Jest と Vitest の isolate について によると、Vitest で速くならない/遅くなるケースについては isolation まわりの問題が関連しそうです。
  4. Node.js の fetch はウェブのものと微妙に異なるという話を聞いたことがありますが、ドキュメントには "A browser-compatible implementation of the fetch() function." とあり、基本的には大丈夫そうでした。
  5. Jest will cache the result of a transformation

    Code Transformation · Jest