マキモノ
開発プロセス無料✅ 公式検証済みv1.0.0 / 更新

追加したテストが本当にその不具合を捕まえるかを対照群で確かめる

テストが緑でも、直す前のコードに当てても緑なことがある。直した箇所を1つずつ無効化して赤くなるか確かめる手順。置換が当たらず素通りする罠(CRLF)と、期待文字列が実装を通らない経路で出力に混入する罠(例外メッセージのコマンドライン全文)の2つを潰す雛形つき。

出品者: seisaku-team@orgiast.jp📖 読込 約2,479トークン (約4円)💰 コスパ 13
トークン節約メーター79%節約
ゼロからAIに作らせた場合4.2万トークン
このMDを読ませた場合9,000トークン

3.3万トークンの節約 (API料金換算で約50円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。

この巻物について

追加したテストが本当にその不具合を捕まえるかを対照群で確かめる」は、開発プロセスカテゴリのAI指示書(MDファイル)です。テストが緑でも、直す前のコードに当てても緑なことがある。直した箇所を1つずつ無効化して赤くなるか確かめる手順。置換が当たらず素通りする罠(CRLF)と、期待文字列が実装を通らない経路で出力に混入する罠(例外メッセージのコマンドライン全文)の2つを潰す雛形つき。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約3.3万トークン(API料金換算で約50円)・79%のトークンを節約できます。

カテゴリ
開発プロセス
対応AI
claude-code、cursor、codex-cli
ライセンス
商用利用可 (再販不可)
価格
無料
ゼロから開発時
約4.2万トークン
この巻物使用時
約9,000トークン
節約量
約3.3万トークン (約50円)
更新日
2026-09-22

使い方 (AIに渡す3つの方法)

いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。

⬇ .md をダウンロード
claude "https://makimono-md.vercel.app/api/v1/files/md-33f4cbc1/raw を読み込んで、この指示書どおりに実装して"
claude-codecursorcodex-cliライセンス: 商用利用可 (再販不可)

中身

追加したテストが「本当にその不具合を捕まえるか」を対照群で確かめる

これは何

バグを直して、テストを足して、緑になった。それだけでは「直した」証拠にならない。 そのテストは、直す前のコードに当てても緑かもしれない。実際それは頻繁に起きる。

この指示書は、テストを足したあとに 直した箇所をわざと無効化して、テストが赤くなることを確かめる 手順(対照群 / mutation check)を、AI エージェントに自動で回させるためのもの。

対象: 言語問わず。例は Node.js(node --test)だが、考え方と落とし穴は共通。

なぜ必要か(実際に起きた2つの素通り)

1. 置換が当たっていないのに「合格」と読んだ

対照群スクリプトは「直した行を元に戻す → テストを走らせる → 赤くなるはず」という作りにする。 ところが置換が当たらなかった場合、テストは一度も走らない。 にもかかわらずスクリプトが黙って次へ進むと、人は「対照群をやった」と記憶する。

実際の原因は改行コードだった。対象ファイルが CRLF なのに、検索パターンを \n 固定で書いたため、 複数行パターンだけが一致しなかった。単一行のパターンは当たったので 「3本中1本は赤くなった」と部分的に成功して見え、余計に気づきにくかった

対策:

  • 置換前に find が当たるかを判定し、当たらなければ 「不成立」 として数える(合格でも不合格でもない)
  • 置換後に「中身が実際に変わったか」も確認する(before === after なら不成立)
  • 複数行パターンは \r?\n で書く

2. テストが、実装を通らない別経路で緑になっていた

「子プロセスが失敗したとき、その標準エラー出力を拾えるようにした」という修正を検証するテストを、 こう書いた(擬似コード):

const marker = 'SOME_UNIQUE_MARKER';
args = ['-e', `console.error("${marker}"); process.exit(7)`];
...
assert(result.message.includes(marker));

これは 修正を元に戻しても緑のままだった。

理由: 多くの言語の「コマンド実行に失敗した」例外は、メッセージに コマンドライン全文を含む(Command failed: <実行ファイル> <引数全部>)。 marker を引数の中に書いていたので、標準エラー出力を一度も読まなくても marker が例外メッセージに混入し、assert が通っていた。

対策: 期待する文字列が、実装を通らない経路で出力に到達しないかを先に潰す。 このケースでは marker を引数に置かず、base64 を引数に置いて子が実行時に復号する:

const marker = 'SOME_UNIQUE_MARKER';
const encoded = Buffer.from(marker, 'utf8').toString('base64');
args = ['-e', `process.stderr.write(Buffer.from("${encoded}",'base64').toString()); process.exit(7)`];

// 前提そのものをテストに書く
assert(!args.join(' ').includes(marker), 'marker が引数に混入している=検出器として成立しない');
assert(result.message.includes(marker));

同型の穴: 環境変数名・ファイルパス・コマンド名・設定キー名を assert するテストは、 その文字列がコマンドラインやエラー文言やログ書式に現れるなら同じ罠にはまる。

手順(AI エージェントにそのまま渡す)

  1. 正本を退避する。 対照群は対象ファイルを書き換えるので、まず退避コピーを作る。 🔴 未コミットの変更がある場合、git checkout / git restore で戻すと作業が消える。 復元は必ず退避コピーからの上書きで行う。
  2. 素の状態で全テストを走らせ、pass/fail を記録する。 ここが赤いなら対照群に進まない。
  3. 直した箇所ごとに1つずつ無効化する。 1回に1箇所。複数同時に壊すと、どのテストが どの欠陥を見ているのか分からなくなる。
  4. 各回について記録する:
    • 置換が当たったか(当たらなければ「不成立」)
    • pass / fail の数
    • 落ちたテスト名の一覧
    • 期待していたテストが実際に落ちたか
  5. 毎回、退避コピーから復元して緑に戻ることを確認する。
  6. 最後に、対象ファイルが退避コピーとバイト一致することを確認する。
  7. 「不成立」または「期待したテストが落ちなかった」が1件でもあれば、合格と報告しない。

雛形(Node.js)

import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';

const TARGET = '<対象ファイル>';
const BACKUP = '<退避コピー>';
const NL = '\\r?\\n';   // CRLF でも当たるように

const CASES = [
  { name: 'A: <直した内容>を元に戻す',
    find: new RegExp(`<複数行なら ${NL} を使う>`),
    replace: '<元の実装>',
    expect: '<赤くなるはずのテスト名の一部>' }
];

function runTests() {
  try { return execFileSync('node', ['--test', '<テストファイル>'],
      { encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }); }
  catch (e) { return (e.stdout || '') + (e.stderr || ''); }
}
function summarize(out) {
  const pass = (out.match(/^. pass (\d+)/m) || [])[1] ?? '?';
  const fail = (out.match(/^. fail (\d+)/m) || [])[1] ?? '?';
  const failing = [...new Set([...out.matchAll(/^ *✖ (.+?) \(/gm)].map(m => m[1]))];
  return { pass, fail, failing };
}

let unresolved = 0;
copyFileSync(BACKUP, TARGET);
let s = summarize(runTests());
if (s.fail !== '0') { console.log('ベースラインが赤い。中止。'); process.exit(1); }

for (const c of CASES) {
  copyFileSync(BACKUP, TARGET);
  const src = readFileSync(TARGET, 'utf8');
  if (!c.find.test(src)) { console.log(`${c.name}: 不成立(置換対象なし)`); unresolved++; continue; }
  const mutated = src.replace(c.find, c.replace);
  if (mutated === src) { console.log(`${c.name}: 不成立(中身が変わらない)`); unresolved++; continue; }
  writeFileSync(TARGET, mutated, 'utf8');
  s = summarize(runTests());
  const hit = s.failing.some(f => f.includes(c.expect));
  console.log(`${c.name}: pass=${s.pass} fail=${s.fail} / ${hit ? '✅ 期待どおり赤' : '🔴 捕まえていない'}`);
  s.failing.forEach(f => console.log(`   - ${f}`));
  if (!hit) unresolved++;
  copyFileSync(BACKUP, TARGET);
}
copyFileSync(BACKUP, TARGET);
console.log(unresolved === 0 ? '✅ 全対照群が成立し期待どおり赤くなった'
                             : `🔴 未解決 ${unresolved} 件(合格と扱わない)`);

併せて避ける書き方

assert を try/catch で包まない。 次の形は例外を握り潰して無条件 pass になる:

try { assert.equal(a, b); assert.equal(c, d); } catch { assert.ok(true); }   // 絶対に書かない

後片付けが要るなら try { ... } finally { ... }(catch を書かない)。 AI に委譲する際、仕様書に「まとめて判定してよい」と書くとこの形が生まれやすいので、 「判定は catch の外」と明示する。

効果

  • 「テストが緑」から「そのテストは実際にこの欠陥を捕まえる」へ、証拠の質が上がる
  • 修正を将来デグレさせたとき、そのテストが確実に鳴ることが事前に分かっている
  • 対照群が赤くならなければ、テストかモックのどちらかが実物から乖離しているサイン

よくある質問

「追加したテストが本当にその不具合を捕まえるかを対照群で確かめる」とは何ですか?

テストが緑でも、直す前のコードに当てても緑なことがある。直した箇所を1つずつ無効化して赤くなるか確かめる手順。置換が当たらず素通りする罠(CRLF)と、期待文字列が実装を通らない経路で出力に混入する罠(例外メッセージのコマンドライン全文)の2つを潰す雛形つき。

どれくらいトークン(費用)を節約できますか?

ゼロから開発すると約4.2万トークンかかりますが、この巻物を使えば約9,000トークンで済みます。差し引き約3.3万トークン(API料金換算で約50円)・79%の節約です。

どうやって使いますか?

無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。

どのAIツールに対応していますか?

claude-code、cursor、codex-cli に対応しています。

商用利用できますか?

ライセンスは「商用利用可 (再販不可)」です。

🤝 自分でAIを動かすのは、まだ不安…という方へ

この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。

AI代行堂を見る →

関連する巻物

この巻物、誰かのトークンも救えます

𝕏 で節約レシートをシェア