Skip to content

Uncovered

ソフトウェア品質管理の仕事に就く前に、テストについて学んだ。組み合わせテストという分野がある。OS × ブラウザ × 解像度 × 認証方式 × 決済手段——パラメータが増えると全組み合わせは指数的に膨れる。全部テストするのは現実的ではない。だからペアワイズ法で「すべての2パラメータ間の値の組み合わせを最低1回カバーする最小セット」を生成する。二因子間網羅。

なぜ2因子間で十分なのか。経験的に、ソフトウェアの不具合の大半は1つまたは2つのパラメータの相互作用で発生する。3因子以上の組み合わせでしか再現しないバグは存在するが、頻度は劇的に下がる。ペアワイズはその境界を突いた実用的な割り切りだ。万能ではない。ただ、全組み合わせが現実的でないとき、もっとも費用対効果が高いトレードオフがここにある。

その代表的なツールがMicrosoftのPICTだ。業界標準だった。いや、いまでも標準だ。2000年代から使われ続けていて、枯れている。C++で書かれていて速い。コマンドラインツールとして完成されている。

ただ、使っていた当時から「生成しかできない」ことが気になっていた。

手書きのテストケースが何百もあるプロジェクトで、最初に知りたかったのは「いまのテストセットがパラメータの組み合わせをどれだけカバーしているか」だった。PICTにその機能はない。生成専用ツールだからだ。足りない分だけ差分で追加したかったが、seedファイルに既存ケースを流し込んで全件再生成する以外に手がなかった。出力はTSVで、パースしないとプログラムから使えない。制約はDSL文字列で書く必要があり、動的に組み立てるのが面倒だった。

PICTが悪いわけではない。PICTの設計思想は「モデルファイルを食わせてテストケースを吐く」コマンドラインツールであり、その範囲では完璧に近い。わたしが欲しかったものが、PICTの守備範囲にはなかっただけだ。

だから一から書き直した。PICTの内部設計に引きずられたくなかったからだ。PICTは制約をExclusion集合として事前に列挙する。あらかじめ禁止される組み合わせをリストアップしておく方式だ。コマンドラインツールとしては合理的だが、「分析」や「差分生成」を後から接ぎ木するには向いていない。

coverwiseの制約エンジンは三値論理で動く。TrueFalse、そしてUnknown。テストケースを1パラメータずつ貪欲法で構築していく途中、まだ値が決まっていないパラメータがある。「OSがWindowsのときbrowserはSafariではない」という制約があっても、OSにまだ値が割り当てられていなければ、制約の評価結果はUnknownだ。PICTのExclusion方式では完全な組み合わせしか判定できない。三値論理なら部分割当の段階で枝刈りができる。制約が複雑なモデルほど、枝刈りの恩恵は大きくなるはずだ。

この設計の根本的な違いが、PICTには存在しない操作を可能にした。

ひとつはanalyzeCoverage。パラメータ定義と既存のテストケース配列を渡すと、カバレッジ率、カバーされていないタプルのリスト、そしてそれぞれが未カバーである理由が返る。os=Linux, browser=Safariが制約により除外、os=Windows, arch=arm64が未テスト。構造化されたオブジェクトとして得られる。PICTにこの機能は存在しない。生成器であって分析器ではないからだ。

もうひとつはextendTests。既存テストを固定したまま、カバレッジ100%に足りない分だけを差分で追加生成する。PICTにもseedファイルはあるが、あれは「この組み合わせを含むテストセット全体を再生成する」もので、「既存を壊さず差分だけ返す」設計ではない。

analyzeで現状を把握し、extendで差分を埋める。このループがcoverwiseの設計の中心だ。

サブモデルの扱いも変えた。PICTのサブモデルは疑似パラメータ変換というアプローチを取る。coverwiseでは各サブモデルが独立したCoverageEngineを持ち、貪欲法のループで全エンジンのスコアを合算して最適化する。「全体は2-wise、決済系パラメータだけ3-wise」という混合強度を、ひとつのループで自然に扱える。

出力も根本から変えた。PICTの出力はTSV——パラメータ名がヘッダ行に並び、値がタブ区切りで続く。プログラムから使うにはパースが必要で、カバーされていないタプルの情報は出力されない。coverwiseの出力は構造化されたオブジェクトだ。テストケースの配列、カバレッジ率、未カバータプルとその理由、改善の提案。入力もJSONオブジェクトをそのまま渡す。DSL文字列を組み立てる必要がない。LLMが生成・消費しやすい形式でもある。

npmにもPICTをラップしたパッケージはいくつかあるし、WASMで動く実装もある。coverwiseはWASMに加えてPure TypeScriptのエントリポイントも持つ。WASMのロードすら不要で、npm installだけで即座に動く。ブラウザでもNode.jsでもDenoでも。バイナリ依存がゼロなら、CIでの追加セットアップも不要だ。

制約のビルダーAPIも用意した。when('os').eq('Windows').then(when('browser').ne('Safari'))のように、TypeScriptの型補完が効く形で制約を組み立てられる。文字列で書くこともできるが、タイポに気づけないし、パラメータ名を変更したときにリファクタリングツールが追従しない。

最初はC++で書いた。AIを使って1日かからなかった。速すぎた。動いてみると、この規模ならTypeScriptで十分な速度が出ることに気づいた。Pure TypeScript版も書き直した。それでも1日だった。以前なら数ヶ月かかっていたと思う。ただ、作る楽しみごと吹き飛んだ感覚があって、達成感より虚無が残った。

以前、テストケースの爆発で苦しんでいるプロジェクトで、テストエンジニアがさっと組み合わせを最適化しているのを見たことがある。あの手際に少し憧れていた。coverwiseは、あの憧れの延長線上にある。

あのときのエンジニアに近づけた感じは、まったくしない。