JavaScriptテストの進化 — JasmineからVitestへ
ブラウザという「本来テストを想定していない場所」から出発したJavaScriptのテスト文化は、Jasmine・Mocha+Chai+Sinon・Karma・QUnitが並び立つ分裂期を経て、2016年のJest大改修で「オールインワン」へと収束した。Testing Libraryの「実装の詳細を避ける」哲学、スナップショットテストの功罪、そしてVite時代のVitestまで、分裂と収束を繰り返したJSテスト史をたどる。
JavaScriptのテストエコシステムは、他言語以上に激しい分裂と収束を繰り返してきた歴史を持つ。理由は明快である。実行環境が長らく「ブラウザ」という、そもそもテストを想定していない場所だったこと、そして言語自体が標準ライブラリに乏しく、小さな部品を組み合わせて使うコミュニティ文化を持っていたことだ。テストランナー・アサーションライブラリ・モックライブラリ・DOM環境シミュレータが別々のパッケージとして存在し、開発者は自分でその組み合わせを選ばなければならなかった。この状態は2010年代半ばにJestの登場でいったん「オールインワン」に収束し、2020年代にはViteエコシステムのVitestが新たな収束点として台頭している。
ブラウザという厄介な出発点
2000年代後半まで、JavaScriptのテストはSeleniumのようなブラウザ自動操作ツールでUIを外側から叩くか、あるいは何もテストしないかのどちらかが主流だった。ユニットテストの文化はJavaやRubyに比べて明らかに根付くのが遅れた。
背景には次のような事情がある。
- DOMや
windowオブジェクトへの依存が強く、Node.js登場(2009年)以前はブラウザ外でJavaScriptを実行する標準的な方法がなかった - モジュールシステムが存在せず(ES ModulesはES6にあたる2015年でようやく標準化)、コードの分離と注入が難しかった
- QUnit(jQuery由来、後述)やJsUnitのような先駆者はいたが、コミュニティ全体の標準にはならなかった
この時期に生まれたJasmine・Mocha・Karma・QUnitが、その後10年以上使われる基礎を作ることになる。
Jasmine — BDDスタイルの先駆者
JasmineはPivotal Labs(現VMware傘下)が開発し、2010年に公開された。Ruby生まれのBDDフレームワークRSpecや、JSSpec・ScrewUnitといった先行ツールに強く影響を受けており、describe / it / expectという自然言語に近い構文でソフトウェアの振る舞いを記述するBDD(Behavior-Driven Development) スタイルをJavaScriptに持ち込んだ。
Jasmineの特徴は「依存が少ないこと」だった。DOMにも他のライブラリにも依存せず、アサーションもモック(スパイ)も内蔵していたため、単体で完結するテストが書けた。この「フレームワーク単体で完結する」という設計思想は、後のJestにも受け継がれることになる。AngularJSの公式テストスタックとしてKarmaとセットで長く使われた。
Mocha + Chai + Sinon — 組み合わせ文化
MochaはTJ Holowaychukが作成し、2011年頃に公開された。Jasmineと異なり、Mochaはテストランナー(describe/itの実行機構)に徹し、アサーションライブラリを内蔵しない「アンオピニオン」な設計を取った。この設計は、Node.jsコミュニティの「小さな部品を組み合わせる」哲学を色濃く反映している。
Mochaは単体では機能が足りず、通常は次の部品と組み合わせて使われた。
| ライブラリ | 役割 | 作者 |
|---|---|---|
| Mocha | テストランナー(describe/it、非同期対応) |
TJ Holowaychuk |
| Chai | アサーションライブラリ(expect/should/assertの3スタイル) |
Jake Luer |
| Sinon.js | スパイ・スタブ・モックライブラリ | Christian Johansen |
Christian Johansenは著書『Test-Driven JavaScript Development』(2010年)の著者でもあり、Sinon.jsはその実践知の集大成として設計された。「Mocha + Chai + Sinon」の組み合わせは2010年代前半から中盤にかけて事実上の定番スタックとなったが、裏を返せば新規プロジェクトのたびに各ライブラリのバージョン整合性やインストール手順を自力で解決する必要があり、「セットアップの重さ」が長らくJavaScriptテストの参入障壁になっていた。
Karma — ブラウザランナーの標準化
ユニットテストのロジック自体はNode.js上で完結できても、「実際のブラウザ(Chrome・Firefox・Safari、当時はIEも)で本当に動くか」を確認したい需要は残る。Karmaはこの課題に応えるテストランナーで、GoogleのAngularJSチームに所属していたVojta Jínaが開発した。当初はTestacularという名称だったが、商標と発音上の問題から2012年にKarmaへ改称された。
Karmaは設定ファイルで対象ブラウザ(実機・ヘッドレス問わず)を指定すると、複数ブラウザでテストを並行実行し結果を集約する。AngularJSの公式テストスタック(Jasmine + Karma)として広まり、「ユニットテストはNode.js内で完結、しかし実ブラウザ検証も自動化する」という二段構えの発想を定着させた。
QUnit — jQuery由来の老舗
QUnitはjQueryの作者John Resigが、jQueryプロジェクト自身のテストコードとして2006年に書き始め、2008年に独立したプロジェクトとして切り出した。当初はjQuery自体に依存していたが、2009年のリライトで完全に独立した。JavaScriptテストツールの中でも最古参の一つであり、現在もjQuery本体やそのプラグインエコシステムの標準テストツールとして使われ続けている。
Jest — ゼロコンフィグの革命
JestはもともとFacebook社内で2011年頃、チャット機能をJavaScriptで書き直すプロジェクトの一環として生まれた。2014年にオープンソース化された当初はJasmineをベースにしており、目立った独自性はまだ薄かった。転機は2016年、Christoph Nakazawaを中心とするチームによる大規模な作り直しである。この年に次の要素が実現し、Jestは「フレームワーク」から「オールインワンのテストプラットフォーム」へと生まれ変わった。
- アサーション・モック・カバレッジ計測をすべて内蔵し、追加ライブラリなしでテストが書ける「ゼロコンフィグ」設計
- jsdomをデフォルトで統合し、実ブラウザなしでDOM APIをシミュレートしてテストを実行
- テストの並列実行(ワーカープロセス分割)による高速化
- ウォッチモード(変更ファイルに関連するテストだけ再実行)
- スナップショットテスト(後述)の導入
この「一つ入れれば全部揃う」という体験は、Mocha + Chai + Sinon + Karmaの組み合わせに疲れていたコミュニティに強く支持され、Reactエコシステムを中心に急速にデファクトスタンダードとなった。2022年にはMeta(旧Facebook)からOpenJS Foundationへプロジェクトが移管されている。
スナップショットテストの功罪
Jestが2016年に導入したスナップショットテストは、関数やコンポーネントの出力をシリアライズして「正解」としてファイルに保存し、以後の実行でその保存済みスナップショットと差分がないかを比較する手法である。Reactコンポーネントのレンダリング結果のような「複雑だが目視確認しづらい出力」の回帰検出に向いており、React Storybookの「Storyshots」など他ツールにも波及した。
一方で運用上の問題も広く指摘されている。
- 差分が出るたびに
--updateSnapshot(俗に「スナップショット更新」)を機械的に叩く癖がつき、レビューされないまま「正解」が書き換わる - 巨大なコンポーネント全体をスナップショットすると、無関係な変更でも差分が大量発生し、誰も差分を読まなくなる
- スナップショット自体は「意図した振る舞い」を主張していないため、間違った出力がそのまま「正解」として固定化されるリスクがある
現在のベストプラクティスは、スナップショットを小さく・意図の明確な範囲に限定し、通常のアサーションと併用することとされる。
Testing Library — 「実装の詳細」を避ける哲学
Testing Library(dom-testing-libraryを土台に、react-testing-libraryなどフレームワーク別ラッパーが乗る構成)は、教育者・エンジニアのKent C. Dodds が2018年に公開した。それ以前の主流だったEnzyme(Airbnb製)が、コンポーネントの内部状態やprivateメソッドを直接検査できる「shallow rendering」を得意としていたのに対し、Dodds はこれを「実装の詳細(implementation detail)に依存するテスト」と批判した。リファクタリングで振る舞いが変わっていないのにテストが壊れる、という問題である。
Testing Libraryの設計哲学は、次の一文に集約される。
“The more your tests resemble the way your software is used, the more confidence they can give you.” (テストがソフトウェアの実際の使われ方に似ているほど、そのテストはより高い確信を与えてくれる)
具体的には、コンポーネントの内部実装ではなく、ユーザーが実際に触れるDOM(ロール・ラベル・表示テキストなど)に対してクエリを行い操作するAPIを提供する。この思想はReactコミュニティを超えてVue・Angular・Svelte向けのラッパーにも広がり、現在では公式にも推奨される標準的アプローチとなっている。Dodds は同時期に、統合テストへの重点投資を説く「テスティングトロフィー」というモデルも提唱しており、その全体像は次々回の記事で扱う。
Vitest — Vite/ESMネイティブの新星
Vitestは、ビルドツールViteのエコシステムから生まれたテストフレームワークで、2021年末に公開された。Viteコアチームのメンバーである Anthony Fu が中心となって開発した。Jestがテストプラットフォームとして支配的地位を築いた後も、次の点で不満が残っていた。
- Jest はBabel変換を通すため、Vite(ESMネイティブ・高速HMR)を使っているプロジェクトとビルドパイプラインが二重化する
- ESM(ES Modules)やTypeScriptのネイティブサポートが後付けで、設定が複雑になりがち
Vitestは Vite の変換・解決パイプラインをそのまま流用することで、Viteプロジェクトとの設定の二重管理をなくし、高速なテスト実行を実現した。APIは意図的にJestとほぼ互換(describe/it/expect、モックAPIも同様)にデザインされており、既存のJestプロジェクトからの移行コストを低く抑えている点も普及を後押しした。
断片化と収束のサイクル
JavaScriptテストツールの歴史は、次のような「分裂→収束→再分裂→再収束」のサイクルとして整理できる。
| 時代 | 状況 |
|---|---|
| 〜2009年 | 標準なし。Selenium等の外部ツールか、独自スクリプトが中心 |
| 2010〜2015年 | Jasmine / Mocha+Chai+Sinon / Karma / QUnit が並立する分裂期 |
| 2016〜2020年 | Jestがオールインワンとして事実上のデファクトに収束 |
| 2018年〜 | Testing Libraryがテストの「書き方の哲学」を再統一 |
| 2021年〜 | Viteの普及に伴いVitestが新たな高速な選択肢として台頭、再分裂と再収束が併存 |
興味深いのは、ツールが変わってもAPIの見た目(describe/it/expect)はほぼ共通したままである点だ。これはJasmineが確立したBDD風の語彙が業界標準として定着し、乗り換えコストを下げていることを意味する。ブラウザを介したE2Eテスト領域の世代交代(Selenium→Cypress→Playwright)は別記事で扱う。
この記事から次の記事へ
JavaScriptという一つの言語の中だけでも、これだけの分裂と収束が起きた。次の記事では視点を広げ、Python・Ruby・Java/Kotlin・Go・Rust・C#/.NET・Swiftという主要言語を横断し、「標準ライブラリに内蔵する」か「サードパーティが事実上の標準になる」かという設計思想の違いを比較する。