zudo-test-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

不在アサーション vs 自分自身のフィクスチャデータ

処理後の出力にマーカー文字列が「含まれないこと」をアサートしつつ、そのフィクスチャ自身が同じ出力へ流れ込むとき、フィクスチャがアサーションを引っかけることがある -- フレイクではなく、CIで初めて失敗する決定論的なアサーション設計の衝突。「フィクスチャツリーを grep する」「不自然なマーカーを使う」の2つのルールが再発を防ぎます。

フィクスチャは出力の一部である

否定の文字列アサーション -- 「この部分文字列は出力に現れてはならない」 -- は、書けるチェックの中でもっとも安全な部類に見えます。ところが、処理対象が自分自身のフィクスチャであり、そのフィクスチャのペイロードがスキャン対象の出力へ流れ込む瞬間、これは罠に変わります。すると、プロダクトがデグレしたからではなく、フィクスチャの作者が、インライン化・コピー・出力されるまさにそのテキストの中に禁止マーカーを書き込んでしまったせいでアサーションが失敗しうるのです。チェックはバグではなく、フィクスチャに対して発火します。

これはフレイクではありません。そしてこの区別こそが、独立したページを与える理由そのものです。レーンが走るたびに同じ形で失敗します -- 入力は同一、出力も同一、アサーションは決定論的です。デフレイキングレシピはここでは何の助けにもなりません。安定させるべきタイミングもなければ、待つべきシグナルもないからです。さらに悪いことに、断続的にではなく100%失敗するため、これはフレイクには見えません -- プロダクトのデグレに見えるのです。修正はフィクスチャとアサーションの設計に宿るのであって、テストのタイミングの近くには一切ありません。

実例: ?raw の不在アサーションが自分のフィクスチャを食べた

あるビルド出力の受け入れテストは、?raw インポート指定子が完全に解決されていること -- 未解決の ?raw クエリ文字列が、出力された client バンドルに生き残っていないこと -- を証明する必要がありました。不在を証明する自然な方法は否定アサーションです。バンドルの本文をスキャンし、部分文字列 "?raw" が消えていることを要求します。

assert_contains_none(body, ["?raw"], "client bundle must not leak an unresolved ?raw specifier")

このテストに供給されるフィクスチャは、raw ペイロードのテキストファイル群でした -- パイプラインが文字列としてインライン化するよう、意図的に ?raw でインポートされるコンテンツです。親切心から、フィクスチャの作者は各ペイロードファイルにそれ自身の説明を書きました。それが何であるか、どこから来たか、どの仕組みを検証するのか。ある1行はこう書かれていました。

sibling workspace ?raw reached from a claimed sub-package host

パイプラインは完璧に動作しました。?raw インポートは解決され、ペイロードのテキストは設計どおりにそのまま client バンドルへインライン化されました -- その結果、フィクスチャ自身の散文にあった ?raw という部分文字列が、そのままバンドル本文へ運び込まれたのです。不在アサーションはバンドルをスキャンし、自分のフィクスチャの説明文の中に居座る ?raw を見つけて失敗しました。同じテスト内の肯定アサーションはすべて -- ペイロードが存在し、インポートが解決され、バンドルがビルドできた -- 通過しました。引っかかったのは否定アサーションだけであり、しかもそれは、欠陥にではなく、うまくインライン化されたフィクスチャに対して引っかかったのです。

一般化された罠

具体を剥ぎ取ると、その形は一般的です。

テストが、処理後の出力にマーカー文字列が含まれないことをアサートし、かつフィクスチャの入力がその出力の中へ流れ込むときはいつでも、フィクスチャはそのマーカーを構造的に含みえないものでなければならない。

この罠に陥りやすいのは、自然な執筆本能に逆らうからです。フィクスチャの作者は、後からフィクスチャを読む人が用途を理解できるように、テスト対象の仕組みを人間可読なペイロードの中で名指しします。?raw フィクスチャは、自分のテキストの中で「?raw」という語を言いたがるのです。その正確な文字列を出力の中で後から禁じるテストは、ペイロードがその出力に到達した途端、自分自身のフィクスチャと衝突コースに乗ります。

この衝突は、あなたが見るであろう3つの場所のうち2つに隠れています。

  • 肯定アサーションは通過するので、パイプラインは正しく見える。

  • フィクスチャは正しく見える -- 可読で自己文書化されており、それこそがフィクスチャを危険にしていた当のもの。

  • 失敗するのは否定アサーションだけで、しかもそれは、実際に原因となったフィクスチャの1行ではなく、出力を指し示す。

ルール1 -- 出荷前に、すべての不在トークンをフィクスチャツリーで grep する

assert_contains_none 系のアサーションを出荷する前に、そのトークンそのものでフィクスチャツリーを grep し、正当に存在してよい場所 -- テストコードとアサーション文字列 -- にのみ現れ、出力へ流れ込むペイロードテキストには決して現れないことを確認します。

grep -rn '?raw' tests/fixtures/raw-payload/

欲しい結果はペイロードファイルからは何も出ないことです -- マッチするのはテストのソースとアサーション自体だけ。フィクスチャディレクトリ配下の .txt.md・データペイロードの中にヒットがあれば、それこそがバグであり、CIがレーンを走らせるより前に捕まえられます。テスト自身のレビューチェックリストに組み込みましょう。新しい不在トークンは、そのフィクスチャツリーを grep してクリーンだと確認するまで完了ではありません。

Warning

grep はリポジトリ全体ではなく、フィクスチャのペイロードに対して走らせなければなりません。リポジトリ全体の grep '?raw' は正当なヒット -- インポート文、アサーション文字列、まさにこのドキュメントページ -- に溺れ、あなたを「無視するよう」訓練してしまいます。実際にスキャン対象の出力へ流れ込むディレクトリに絞り込めば、どのマッチも本物の衝突になります。

ルール2 -- 存在確認には不自然なマーカーを、不在確認には構文トークンを

より深い修正は、正反対の2つの仕事に同じ語彙を使うのをやめることです。

  • 肯定アサーション -- 「このマーカーは現れなければならない」 -- は、散文の中に自然には出現しえないトークンを鍵にすべきです。ZFB_MARKER_RAW_INLINED のような大文字のセンチネルなら、説明的な文・コメント・隣り合うフィクスチャの中に偶然現れることはないので、それに対する存在確認は曖昧さがありません。

  • 不在アサーション -- 「このトークンは現れてはならない」 -- は、出力が本当に含んではならない構文トークン?raw、インポート指定子、テンプレートのプレースホルダ)のために取っておくべきです。そのうえで、規律はルール1の鏡像になります。それらの構文トークンをすべてのペイロードファイルの散文から締め出し、出力に到達しうる唯一の経路が、あなたが追っているバグだけになるようにするのです。

まとめると、フィクスチャは、そのアサーションが探し求めるセンチネルで説明し、アサーションが探し咎める構文トークンでは決して説明しないこと。?raw を検証しなければならないペイロードは、テスト側から ?rawインポートすべきであって、本文で「?raw」という語を語るべきではありません。

ZFB_MARKER_RAW_INLINED payload body, imported via the raw specifier from the test side

上のフィクスチャは、チェックされる対象のセンチネルだけを述べ、チェックで咎められる構文トークンは決して述べません -- だからインライン化されたペイロードは、否定アサーションを一度も引っかけることなく肯定アサーションを満たせます。

なぜCIで初めて失敗するのか -- 検知の非対称性

ビルド出力の受け入れレーンは重い処理です。実際のバンドルをビルドし、出力されたバイト列をスキャンするため、ローカルの保存ごとに走らせるのではなく、env でゲートされるか、重い実行ティアに予約されるのが一般的です。そのレーンがスキップされる場所ではどこでも、この種のバグは通過してしまいます -- バンドルが実際にビルドされスキャンされるまで、それを捕まえるものが何もないのです。

その帰結が検知の非対称性です。この失敗は、ゲートされたレーンが初めて露出するCIで初回に現れるのが特徴的で、しかもプロダクトのデグレの装いをまといます。「未解決の ?raw が client バンドルに漏れた」はリゾルバのバグに読め、自然な本能はプロダクトの解決パイプラインを探しに行かせます -- そこは完璧に動いているのに。無駄になったCIの1周は安いコストです。高くつくのは、フィクスチャ作成のバグがプロダクトのバグに化けたせいで、間違ったサブシステムをデバッグしてしまうことです。

Note

最初の手掛かりとしては有用ですが、それ単独では証明になりません。同じテスト内の肯定アサーションがすべて通過し、失敗するのは不在アサーションだけという点です。手掛かりに留まるのは、部分的なリゾルバのデグレ -- 解決された多数の指定子に混じって1つだけ未解決 -- でも、すべての肯定チェックが緑のまま不在アサーションだけが赤くなりうるからです。この手掛かりは疑いをフィクスチャへ絞り込みはしますが、決着はつけません。決着をつけるのはルール1のgrepです。禁止トークンがフィクスチャのペイロード内に見つかればフィクスチャ汚染が証明され、フィクスチャのどこにも見つからなければプロダクトが本当にマーカーを漏らしたことになります。パイプラインが正しいと結論する前に、その grep を走らせましょう。

2つのルールを合わせて

  1. テストを出荷する前に、すべての不在トークンをフィクスチャツリーで grep する。 禁止文字列はテストコードとアサーション文字列に現れてよい。スキャン対象の出力へ流れ込むペイロードテキストには決して現れてはならない。

  2. アサーションの極性で語彙を分ける。 存在確認は散文に現れえない不自然なセンチネル(ZFB_MARKER_...)を鍵にし、不在確認は構文トークンのために取っておき、その構文トークンはすべてのペイロードの散文から締め出す。

どちらのルールも、たった1つの不変条件に奉仕します。フィクスチャは、自分のテストが禁じるマーカーを構造的に生み出せないものでなければならない。そこさえ正しくできれば、不在アサーションは、それを書いた本来の目的である欠陥に対してしか失敗しえなくなります。

Revision History

作成更新