Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

10,000超えのケースを持つ膨大なテストをJestからVitestへ1日で移行した

Advent Calendarのサムネイル。「10,000超えのケースを持つ膨大なテストをJestからVitestへ1日で移行した」というタイトルと、その上に「テスト」「Claude Code」というタグが付与されている 初めまして。技術本部Digitization部Entry Engineeringグループの薩田です。 Sansan Advent Calendar 2025 4日目です。

私のチームの主要システムはTypeScriptで書かれたシステムです。そんなシステムのテストフレームワークをJestからVitestへ移行しました。 テストファイルが700を超える規模のシステムですが、AI Coding Agent(今回はClaude Code)を活用することで効率的に移行を完了できました。

本記事では、なぜJestからVitestへ移行したのか、そしてAIをどのように活用したのかについて紹介します。

Jestの課題

Jest は長らくJavaScript/TypeScriptのテストフレームワークとして定番の地位を築いてきました。私自身、何年もお世話になってきたのですが、いくつかの課題を感じるようになりました。

ESM周りの問題

一番大きな課題はESM(ECMAScript Modules)周りです。

JestはCommonJS時代に設計されたこともあり、現在でもESMは実験的な機能です

例えば最近(といっても3カ月前ですが)ではテスト用データを作成するFakerもESM-onlyなパッケージとなりました1。 ESMで書かれたライブラリをインポートする場合、変換の設定がうまくいかずエラーになることがあります。これを解決するために設定を追加すると、今度は別のライブラリで問題が発生する...という状況に陥ることもありました。そういったESM Onlyなパッケージが出てくるたびにJestをなんとかESMに対応できないか試行錯誤していましたがうまくいきませんでした。

メモリ消費

テストファイルが増えてくると、Jestのメモリ消費が気になります。

2年ほど前はCIでメモリ不足によってテストが落ちることもあり、テストを分割して実行したりする必要がありました。 時間的制約の問題の解消も兼ねていますが、CIでは現在10分割で行っています。実際のRAMの利用量を確認するとそれぞれが常に80%程度を利用している状態です。

改善前のCircleCIのResourcesのメモリ利用量の線グラフ。テストが10分割で実行されているので、10個の線があり、それぞれ全て80%前後を推移している。
改善前のCircleCIのResourcesのメモリ利用量

なぜVitestか

Vitest を選んだ理由はいくつかあります。

ESMネイティブサポート

Vitestは実行時にViteによるトランスパイルを実行します。そのためViteの強力で高速なモジュール解決の恩恵を受けられるため、ESM周りの設定で悩むことがほとんどありません。

Jestとの互換性

VitestのAPIはJestに非常に似ています。describeitexpectといった基本的なAPIはほぼ同じですし、vi.fn()vi.mock()といったモック関連のAPIも似た形で提供されています。

これにより、移行のハードルが下がります。多くのテストコードは、インポート文を変更する程度で動作するようになります。

AIを活用した移行戦略

さて、ここからが本記事のメインです。

私たちのシステムでは700ファイル・14,000ケースを超える単体テストが存在します。 先程Vitestの利点においてJestとの互換性の面を挙げましたが、特にCJSとESMの違いからモック周りの挙動など完全なる互換性がない部分もあります。そのためVitestに置き換えたからと言ってそのまま動くわけではありません。私たちのシステムでは事情によりモックを利用している部分が多く、Vitestに置き換えた後、多大な修正が必要となります。

結果的にはjest.spyOnvi.spyOnに書き換えるだけの軽微な修正も含めると300ファイルを超える修正となりました。しかしこの修正は1日で完了しました。

その秘訣は何でしょうか。答えは「Coding Agentを最大限に活用する」です。

Coding Agentの発展は非常に早く、もはやその利便性は明記する必要もないと思います。一方でCoding Agentによる生産性を高めるのも活用方法次第であり、まさに「AIとハサミは使いよう」であることも、もはや常識かなと思います。 つまり「Vitest移行して」と言うだけでは、もしかしたらうまくいくかもしれませんが、おそらくかなり中途半端な結果になると思います。加えてAIによる修正のレビューも、特に300ファイルを超えるファイルを見るのならば非常に時間がかかってしまいます。

今回は次のように工夫することで効率的に移行が完了しました。

テストファイルの分割

まず、テストファイルを20分割しました。

find __tests__ -name "*.test.ts" | split -l 20 -d -a 2 - tmp/test-files/files-

分割したファイルリストをtmp/test-files/以下に配置し、それぞれの完了状況をdone-list.txtで管理します。

分割する理由としては、実際にテストを実行させるときに実行時間を減らすためです。700ファイルを一気に実行していくと凄まじく時間がかかります。当然1ファイルづつ実行するのが一番早いですが、そうなると最低でも700回テストを実行してその結果を確認するようになり、AIのコンテキスト的に管理が危ういと感じました。折衷案的に分割したファイルに対してテストを実行させることで、テストの時間を短縮しつつテストの総実行回数も減らすようにしました。

Claude Codeに渡したプロンプト

次のようなファイルをClaude Codeに渡して、テストの修正をしました。

# テストを修正する

あなたの目的はJestからVitestへの移行でになるタイミングでエラーになるテストを修正することです。
あなたは下記のループを実行し、テストがすべて通るまで下記を行い続けます。

1. tmp/test-files/done-list.txt の中から、チェックマークがついていないファイルを選択する
2. `yarn test --bail=1 $(cat tmp/test-files/<1で選択したファイル>)` を実行し、エラーになるテストを探す
3. 該当のエラーの調査を行う
4. 該当のファイル以外に、似たようなエラーが発生する可能性があるファイルを探しだす
5. それらのファイルをすべて修正する
6. どのようなエラーが発生したか、原因はなにか、どのように解決したか、どのファイルを修正したかを tmp/docs/fix-tests-list.md に記載してください
7. 修正したファイルのみをテストし、エラーが出ないことを確認します。エラーが出る場合は3から再度行います
8. すべてのテストが通ったら、tmp/test-files/done-list.txt の該当のファイルにチェックを付けます
9. この tmp/docs/fix-test.md ファイルをもう一度読み直します
10. tmp/test-files/done-list.txt のすべてのチェックが通るまでこのサイクルを行い続けます。

ちなみに done-list.txt はこんな感じです。先程分割したファイル名が入ります。

- [x] files-01
- [x] files-02
- [ ] files-03
- [ ] files-04
...

このプロンプトのポイントは次の通りです。

ループ構造を明示

Claude Codeを主に活用している方なら経験があるかもしれませんが、Claude Codeは全くプロンプトがないと「AからBまで全部を行って」というと「AからBの途中まで行いました」といって作業を停止することが多いです2。「続けて」と言えば続きますが、今回のように時間のかかることが予想される場合、できれば人間の介入なしにタスクを完了してほしいです。

「10. tmp/test-files/done-list.txtのすべてのチェックが通るまでこのサイクルを行い続けます」により完了条件とサイクルを回すことを明示的にするのは効果があったのか、Claude Codeは累計5時間ほど実行していました。実際にはClaude Codeは一度だけ「files-07まで行いました」と言って止まったので2時間と3時間ぐらいの稼働でした。 また、最近では改善されているかもしれませんが、昔のすぐに命令を忘れるお転婆Claude Codeの経験から「9. このtmp/docs/fix-test.mdファイルをもう一度読み直します」によりサイクルを回しているうちに真の依頼を忘れないようにしています。

エラーの横展開

Vitestに限らず、なんらかの移行時には、似たようなエラーがさまざまなファイルで発生する場合があります。今回であれば jest.spyOnvi.spyOn に書き換える必要があるなどそういったものです。テストの実行に時間的コストが掛かるため、該当のファイルをテストして初めてこのエラーにぶつかって解消するというものは非常にコスパが悪いです。 「4. 該当のファイル以外に、似たようなエラーが発生する可能性があるファイルを探しだす」と「5. それらのファイルをすべて修正する」によって、一度発生したテストを横展開し、修正させることで時間的コストの抑制を試みました。後半の方になると実際は同じエラーなんだけど見つけづらいみたいなことはありましたが、簡単なエラーであれば横展開して一気にテストを修正していました。

記録を残す

「6. どのようなエラーが発生したか、原因はなにか、どのように解決したか、どのファイルを修正したかをtmp/docs/fix-tests-list.mdに記載してください」という指示文により、AIによる修正をログとして残しました。 300ファイルの修正とは言えど、基本的には同じようなエラーが発生します。そのため300ファイルをちゃんと読むよりも、エラーの種別と原因、解決方法をリストアップしてそれをレビューするほうが人間にとっても効率的です。

実際に発生した問題と解決方法の例

具体的にどのようなエラーがでて、どのようにAIは解決したのか、何個か例を記載します。ちなみに解決方法は、(私もレビューはしていますが)記録を残すでAIが出力したものを載せています。ちゃんと詳細にAIが書き出してくれているんだなということを感じていただければ幸いです。

vi.fn() のモックがコンストラクタとして機能しない

原因

Vitestでは、mockImplementation(() => ({...})) のようなアロー関数を使ったモックは、コンストラクタ (new) として呼び出すことができません。

警告メッセージ:

[vitest] The vi.fn() mock did not use 'function' or 'class' in its implementation

エラーメッセージ:

TypeError: () => ({...}) is not a constructor

これはJavaScriptの仕様に起因するものです。アロー関数は[[Construct]]内部メソッドを持たないため、newで呼び出すことができません。Jestではこの制約を内部的に回避していたようですが、Vitestではより厳格に扱っています。

解決方法

アロー関数を通常の関数式に変更します。

Before (エラー):

(SomeClass as unknown as Mock).mockImplementation(() => ({
  method: vi.fn(),
}));

After (修正後):

(SomeClass as unknown as Mock).mockImplementation(function () {
  return {
    method: vi.fn(),
  };
});

fetch-mock の相対URL

正確にはfetch-mock/vitestを利用するように事前に修正しており、それによるエラーの解消です。

原因

@fetch-mock/vitest の新しいバージョンでは相対URLがデフォルトでサポートされなくなりました。

解決方法

相対URLの許可:

fetchMock.config.allowRelativeUrls = true;

成果

AIを活用してVitestに移行することで少なくともテストにおいてはESMに縛られることがなくなりました。

また副次的とは言えど、メモリ使用量に関しては大きな改善がありました。最初に触れた通り80%前後(5GB)程度を常に使用していたものが、なんと25%程度(2GB)程度まで下がりました。

再掲された改善前のCircleCIのResourcesのメモリ利用量の線グラフ。テストが10分割で実行されているので、10個の線があり、それぞれ全て80%前後を推移している。改善後のCircleCIのResourcesのメモリ利用量の線グラフ。テストが10分割で実行されているので、10個の線があり、それぞれ全て25%前後を推移している。
改善前と改善後のメモリ利用量

実行時間に関しても改善前のCIの実行時間が平均8分程度であり、改善後は平均6分程度と約2分ほど改善されました

まとめ

JestからVitestへの移行をAI(Claude Code)を活用して行いました。

テストに限らず、さまざまなものを移行するときや大量のファイルに対して変更するような作業はAIの得意分野です。その作業を確実に行うために、月並みではありますが改めて指示を明確にするAIの成果をフィードバックできる仕組みがある(今回はテストやビルドなど)AIが上手く動作するためのちょっとした準備(今回で言えばファイル分割など)が非常に重要だと感じました。 加えて、レビューをしやすい仕組みのためにAIの作業報告の仕組みを導入すると、特に今回のような単純作業かつ多量の修正がある場合は効果が高いと感じました。


  1. アップグレードガイドでは require を利用することで引き続きCommonJS環境で動かせると記載はあります。
  2. Codexに関してはそういった途中でタスクを止めることは少ない印象です。Sansanの技術本部ではCodexもClaude Codeもどちらも利用できますが、たまたまCodexを別のタスクに利用していたので、今回は消去法的にClaude Codeを利用しました。ただ今回の方法は特定のCoding Agentに留まらずに幅広く利用できると思います。

© Sansan, Inc.