フューチャー技術ブログ

テスト連載2026を開始します / AIがテストを書く中で、「あるべき仕様」を単体テストで担保するために工夫してみていること

こんにちは。CSIG の市川です。普段は FutureVuls という脆弱性・サプライチェーンリスク管理 SaaS の開発を担当しています。

今年から、初の試みとして「テスト連載」を実施します!🎉
AI 時代において、重要度が増すテスト技術、動作確認、品質保証などをテーマとした連載です。
今回は 5 人の方が参加します。

日付 執筆者 タイトル/テーマ
6/15(月) 市川裕也さん AI がテストを書く中で、「あるべき仕様」を単体テストで担保するために工夫してみていること
6/16(火) 澁川喜規さん 生成AIに任せたら何でテストを書く?AIに聞いてみた
6/17(水) 武田大輝さん TBD
6/18(木) 清水利博さん 非機能テストを記号接地する
6/19(金) 佐藤尭彰さん testlist の大切さと、testリストを割と柔軟に操作できる話

本記事は、テスト連載2026 の 1 日目の記事です。

はじめに

最近とあるソフトウェアを 2 か月くらいかけて 0 から AI で構築しており、この開発のなかで「テストを AI にどう書かせるか、どうやってテスト品質とコード品質を担保すれば良いか」を考えることが増えました。
今回はそのあたりで自分が考えていることを書き、実際にどのような運用を試してみているか(作成したプロンプト/skill や使用するタイミング、開発フローなど)を紹介します。

今回紹介する方法は、「AI で開発のスピードを出来る限り高めて、生産量を最大化する」という手法ではなく、品質を担保するためにある程度スピードを落とすものです。 (世間の流れからは逆行しているかもしれないです)
自分も AI とのかかわり方は随時アップデートしていきたいと考えているため、このやり方や思想の是非を含め、もしご意見あればぜひコメント等いただけますと幸いです。

本記事の中でもし参考になる部分があれば、必要な範囲で取り入れてみていただければと思います。

TL;DR

  • AI が実装もテストも自律的に書くようになり、生成物を全部レビューするのは現実的でなくなった。品質を保つ手段として、単体テストが一層重要になる
  • ただし「あるべき振る舞い」を決めるのは人間であり、テストはその仕様書。ここは AI に任せきれない、人間が握り続けるべき部分
  • AI に任せると 2 つが崩れる。 (A) 振る舞いの記述が読みづらく、何を担保しているか人間が確認できない。 (B) 基準がないと写経テストや不要なテストが量産され、品質が下がる
  • 対処として、「振る舞いの読みやすさのルール」と「何をテストし、何をテストしないか」を 1 つの markdown(test-design.md)にまとめ、AI に常に読ませている
  • 一定の効果はあるが、確認の手間など課題も残っている

想定読者

  • AI 時代において、単体テストの品質保証で悩んでいる人
  • AI にどのようなテストを書かせるべきか / 書かせないべきかを悩んでいる人
  • AI の出力する日本語が読みづらく、振る舞いを理解するのに困っている人

スコープ外の論点

  • E2E テスト
  • DDD 等の設計手法自体 (コードの品質を保つ、という観点ではこれも重要だと思いますが、本記事では扱いません)
  • 単体テストのカバレッジの上げ方
  • AI が、テストを通すためにテストを勝手に書き換えてしまう問題への対処方法

AI が自律して実装するようになった中で、品質をどう保つか

ここ半年ほどで、AI はかなり自律的に動くようになりました。plan mode で作りたい機能を渡して、出てきた実装計画を auto mode で実装させれば、コードもテストも最後まで自律して書いてくれます。

この変化により、単位時間あたりのコード生成量は大きく増えました。それに伴い、生成されたコードを全部読んでレビューするのは、認知負荷的にもスピード的にもかなり厳しくなってきました。
かといって、中身を見ずにソフトウェアに取り込み続けると、気づいたときには自分でも仕様が正しいか把握できない、変更容易性の低いソフトウェアが出来上がっている、ということになりかねません。

では、今までの何倍ものスピードで開発していくなかで、どうやって品質を保てばいいのでしょうか。色々観点はあるかと思いますが。自分は「単体テストにより品質を保つ」ことが重要なポイントの一つだと考えています。AI が量産するロジックの退行 (バグ) を最も簡単かつ継続的に検出できるのが単体テストだからです。

本記事では、AI 時代において、単体テストの品質を保つ方法について、持論を述べていきます。

前提と、自分が大事だと考えている 2 つの主張

前提: あるべき仕様は人間が決め、それを自動テストで継続的に担保する

まず前提として、あるべき仕様、あるべき振る舞いは人間が決めるものです。この部分は、AI 時代においても不変でしょう。(この前提がそもそもずれてきていたらすみません、コメント等いただきたいです)

そして、この「あるべき仕様、あるべき振る舞い」は、テストコードによって担保することが望ましいです。テストコードは、そのソフトウェアがどう振る舞うべきかを伝える、いわば「仕様書」でもあります。

品質を担保する上では、あるべき振る舞いが達成され続けているかを自動テストで継続的に担保していくことが、生成スピードが上がった今こそより重要になってくると思います。

この前提から導かれる 2 つの主張

AI にコード・テスト生成を任せながら、「自動テストで品質を継続的に担保する」ことを成立させるには、次の 2 つを満たす必要があります。

  • A: 実装計画やテストコードから、あるべき振る舞いを把握しやすい形にしておく (振る舞いの記述の readability をあげる)
  • B: そのテストコード自体の品質を保つ

何も対策をしないと、AI に任せたときに以下の表に記載したような課題が顕在化し、 A も B も崩れます。
この主張・課題と、課題への対処を、先に表にまとめておきます。

あるべき姿 AI に任せたときの課題 どう対処するか
A あるべき振る舞いを、plan / テストコードから把握しやすくしておく AI が出す「確認すべき振る舞い」の説明が分かりづらく、担保できているか人間が確認しづらい 振る舞いの「読みやすさのルール」を決める
B テストコード自体の品質を保つ 品質の基準を与えないと、写経テストや不要なテストが量産され、保守性やリファクタリングへの耐性が下がる 振る舞いをテストさせる /「何をテストし、何をテストしないか」を決める

以降、A・B それぞれについて「なぜ大事か → AI に任せると何が問題か → どう対処してみているか」の順に見ていきます。

A: 振る舞いを把握しやすい状態にする

なぜ「把握しやすさ」が必要なのか

テストが「あるべき振る舞い」を担保できているかを人間が判断するには、まず、そのテストがどんな振る舞いを確認しようとしているのかが人間にとって把握しやすい状態でなければいけません。

テスト内容を人間が把握しづらい場合、「確認したつもり」で中身を流してしまい、結局は何も担保できていない、という状態になる恐れがあります。

課題A: AI が出力する「確認すべき振る舞い」は分かりづらい

何も指示しないと、AI が出力する「振る舞い」の説明は、英語を機械的に和訳しただけのような日本語になったり、実装の中身に踏み込んだ書き方になったりしがちです。

こうなると、その振る舞いの説明を読んでも「結局このテストは何を保証したいのか」が頭に入ってこず、人間がチェックする役割を果たせません。

対処A: 読みやすさのルールを決めておく

そこで自分は、 plan 時に振る舞いの説明を AI に書いてもらうようにし、その際次のような点を守って書かせるようにしています。

  • コード内の変数名や内部のデータ構造を、説明にそのまま持ち込まない
  • そのコードベースを知らない人(たとえばドメインに詳しい非エンジニア)が読んでも理解できる日本語にする
  • 体言止めで止めず、「何をしたら、どうなれば正しいのか」を文章で言い切る

テストコード内のテストケース名やコメントも、同様の観点を守らせています。
この運用の効果については、「上記の考え方を、どのように開発フローに反映させているか」の節に記載しています。

B: テストコードの品質を保つ

前提: そもそも「品質の高いテスト」とは何か

テストにおいてどのような観点を大切にすべきかは、作っているソフトウェアによって異なると思います。
下記の観点には過不足があると思うので、あくまで参考として捉えていただければと思います。

結論から言うと、以下の 2 点は「品質の高いテスト」を満たすための重要な要素であると考えています。

  • ① 実装の詳細に立ち入らず、振る舞いを確認していること
  • ② テストする価値が高いコードに絞って、テストを記述していること

以下、『単体テストの考え方/使い方』という書籍を下敷きにし、この理由を記載します。

https://amzn.asia/d/04vVsRk9

同書では、良い単体テストは次の 4 つの要素で評価できるとされています。

  • 退行(≒ バグ)に対する保護
  • リファクタリングへの耐性
  • 迅速なフィードバック
  • 保守のしやすさ

① なぜ振る舞いをテストする必要があるか

上記 4 点のうち、特に意識したいのが「リファクタリングへの耐性」です。
これはざっくり言うと「偽陽性の少なさ」です。偽陽性とは、実装は正しく振る舞っているのに、テストが落ちてしまう状態を指します。

偽陽性が増えると、開発者は次第にテストの結果を信用しなくなります。「また誤検知だろう」と落ちたテストを軽く扱うようになり、テストの存在意義が薄れていきます。また、何が正しい振る舞いかが分かりづらくなるため、変更容易性が低くなっていきます。

そして、この偽陽性は、テストが実装の詳細を確認してしまっているときに起きやすくなります。実装の詳細に立ち入っていると、実装の中身が少し変わっただけで、振る舞いは正しいのにテストが落ちる、という状況が発生しやすくなります。そのため、「実装の詳細」に立ち入らず、「振る舞い」のみを確認するテストを書くことが重要なのです。

② なぜテストする価値が高いコードに絞るべきか

これは、主に「保守のしやすさ」の観点で重要です。

「実装の中身をそのまま書き写した写経テスト」のような、取るに足らないテストが多いと、「肝心の振る舞いがちゃんと確認できているか」を人間が読み取る邪魔になります。そのようなテストは、段々保守されなくなっていきます。

継続的に保守していく、という観点において、「不要なテストを書かない」ことも重要です。

課題B: 基準を与えないと、写経テストや不要なテストが量産される

テストについて特に基準を与えないと、AI は次のようなテストを書きがちです。

  • 実装の中身をそのまま書き写したような「写経」テスト
  • 「振る舞い」ではなく「実装の詳細」を確認するテスト

先ほどの 4 要素で言えば、これらは「リファクタリングへの耐性」や「保守のしやすさ」を下げる、質の悪いテストです。
さらに、AI に任せると際限なくテストを増やせてしまうので、取るに足らないテストも大量に生成されがちです。
こうしたノイズが多いと、「肝心の振る舞いがちゃんと確認できているか」を人間が読み取る邪魔になります。テストは多ければ多いほど良い、というわけではありません。

対処B: 振る舞いをテストさせる /「何をテストし、何をテストしないか」を決める

そこで、「実装の詳細」ではなく、「振る舞い」をテストさせるようにします。
具体的には、

  • 可能な限り出力値ベース・テストを優先する (状態ベース・テストやコミュニケーション・ベース・テストは最小限にする)
  • 期待値はリテラルで直書きする
  • モック系とスタブ系を使い分ける。外に出る副作用が存在しないときに、そのオブジェクトが呼ばれたかの検証は行わない

また、コードの性質によってテストの厚みを変えるようにしています。

  • ドメインロジック / アルゴリズムは厚めにテストさせています。境界値、条件の組み合わせ、状態遷移、壊れやすい入力など、確認すべき振る舞いを網羅的に押さえます。ここはテストしやすく、かつバグが下流に波及しやすいからです。
  • usecase 層 / コントローラ層の処理は、代表的なハッピーパスと、純粋ロジック側では確認できない「複数の処理を組み合わせたときにだけ起きる異常系」だけで十分、としています。個々の値の網羅は、純粋ロジック側で済んでいるはずだからです。
  • それ以外の 取るに足らないコードはテストしないようにします。

「何をテストしないか」をはっきり決めておくことが、不要なテストの量産を抑え、結果として「担保すべき振る舞い」を見通しよく保つことに繋がります。

上記の考え方を、どのように開発フローに反映させているか

markdown にまとめる

A(振る舞いの読みやすさ)も B(テストの品質基準)も、毎回 AI に口頭で伝えるのは大変です。
そこで自分は、これらのテスト方針を 1 つの markdown(test-design.md)にまとめ、plan mode 時やレビュー時に AI に自動で読ませるようにしました。

実際に使っている test-design.md(特定プロジェクトに依存する記述を削った一般化版)

実物から、プロジェクトの構成やコードに依存する部分を取り除き、どのプロジェクトでも読めるように一般化したものです。雰囲気が伝わればと思って載せています。

**原則**

- **振る舞い(契約)をテストする。** 呼び出し側・利用者から見た結果を確認する。実装の詳細(関数名・内部データ構造・テスト手法)はテストしない。実装が変わっても契約が保たれていれば通り、バグが入れば落ちる ―― これを目標にする。
- **判定軸: そのテストは「バグ」で落ちるか、「意図した編集」で落ちるか。** 意図した編集でだけ落ちるテスト(=実装のデータをテスト側に書き写しただけの写経テスト)は書かない。バグを捕まえず、直す箇所が 2 つに増える保守コストだけが残る。
- **壊れにくさを最優先する。** 退行検出力(壊れたら気づく)と壊れにくさ(意図した編集では落ちない)が両立しないときは、壊れにくさを守る。
- **assert の無いテストを書かない。** 値を出力するだけのものはテストではない。
- **出力値ベースを優先する。** 「この入力でこの出力/エラーになる」を確認するのが基本。状態ベース・相互作用ベース(モックで「こう呼ばれたか」を見る)は、それでしか確認できない振る舞いのときだけ最小限に使う。
- **読み手に「何を確認するテストか」が伝わること。** 読み手は実装を知らない同僚だと思って、テスト名・コメント・計画の「確認すべき振る舞い」を書く。
- **テストしやすさはコード設計のサイン。** 1 つの操作の振る舞いに落ちず、複数操作の組み合わせをテストする羽目になったら、確認すべき振る舞いの定義が雑か、実装が複雑すぎる。後者ならテストを捻る前にコードのリファクタを検討する。

**記述スタイル**

各項目は「何をしたら/どうなれば正しいか」を 1 文で書く。実装・テスト用語で説明せず、呼び出し側・利用者から見た結果で書く。読みやすさの規則:

1. 造語・略語は初出で定義するか、既知の言葉に置き換える。実装内部の言葉より利用者の言葉を選ぶ。
2. 変数名・式を文の主語・述語にしない。日本語で言い切ってから、コードや式は括弧で参照として添える。
3. 状態は動詞化する(「=true」で止めない)。主語と助詞を省略しない。
4. 1 項目=1 振る舞い。条件が複数あるなら箇条書きを割る。
5. 括弧の中に括弧を入れない。補足を主文の途中に挟まない。
6. 出自メモや相互参照を振る舞いの文に混ぜない。

Go の場合は、テスト関数名は英語にし、振る舞いはその関数の直上に日本語コメントで 1 文書く。`t.Run(name, …)` のサブテスト名は日本語でよく、ここが「何をしたらどうなれば正しいか」を読み手に伝える主役になる。

**観点リスト(対象の性質に合うものを選ぶ)**

- 境界値(空/最小/最大/オフバイワン)
- 同値分割(入力を同じ振る舞いのクラスに分け、各クラスの代表を 1 つ)
- decision table(条件の組み合わせで結果が変わるロジックの網羅)
- 状態遷移(許される遷移と許されない遷移)
- エラー推測(nil・ゼロ値・重複・順序・並行・桁あふれなど、経験上壊れやすい所)
- 往復(encode→decode / write→read で欠落・破損しない)
- 不変条件(「壊れていたら必ずバグ」な性質。例: 識別子が重複しない)
- キャンセル/タイムアウト(打ち切りと中断を区別する)
- 部分失敗の継続方針(途中でエラーが出たとき、止めるのか続けるのか)
- 冪等性(同じ操作を 2 回実行しても結果が変わらない。必要な処理のみ)
- 環境依存の注入(時刻・乱数・実行環境を外から渡して決定論的にする)

**網羅度の出し分け(純粋ロジックは厚く、繋ぎ・I/O は薄く)**

- **純粋ロジック(ドメイン層など)** ― 観点を網羅的に。境界値・decision table・状態遷移・エラー推測などを厚く押さえる。
- **orchestration / 繋ぎ(ユースケース層など)** ― 代表的なハッピーパス+異常ケースの代表で足りる。純粋ロジック側で網羅済みの値を、繋ぎ側で重複してフル比較しない。
- **presentation(CLI・出力フォーマットなど)** ― ほぼテストしない。入力検証に固有のロジックがあるなら、純粋な検証関数に寄せて出力値ベースでテストする。出力フォーマットは構造(ヘッダ・列順・行数)だけ確認し、各セルの値は計算側の関数のテストに委ねる。
- **I/O 実装(DB・ファイル・外部 API など)** ― 結合寄り。本物の依存に対する確認か、契約を模した fake で確認する。

**unit と結合の境界**

- 外部サービスとの契約(API メソッド名・送信 JSON など)は unit で検証しない。本物に投げないと真偽が分からないので、結合テストか不変条件に寄せる。送信内容をテスト側に再記述する写経テストは書かない。
- スナップショットはグレー。外部契約の写しは unit では真偽を検証できない。
- 不変条件テストは「壊れていたら必ずバグ」になる構造的性質だけ網羅してよい。それ以外の値の写経は書かない。

**外部 I/O を含むテストの書き方**

- **境界で分離する。** 「データをどう組み立て/解釈するか(ロジック)」と「実際に読み書きする(I/O)」を分ける。ロジックは純粋にテストできる形に寄せる。
- **ファイルは round-trip で見る。** 書き出して読み戻すと欠落しないことを確認する。実ファイルが要るなら一時ディレクトリを使う。
- **期待値はリテラルで直書きする。** 実装と同じ計算式でテスト内に期待値を作らない(実装とテストが同時に間違えても気づけない)。
- **テストダブルの使い分け。** 既定は stub / fake。データ取得のダブルは「呼ばれたか」を検証しない。mock / spy は、外部への副作用の送出そのものが契約のときだけ最小限に使う。fake は本物の契約を模倣する。

**確認しないと決めた振る舞いも書く**

「価値の高いテストだけ残す」ためには、確認することと同じくらい、確認しないと決めたことが重要になる。計画・レビューでは「確認すべき振る舞い」の一覧の後に「確認しないと決めた振る舞い」の一覧を併記し、各項目に除外理由を 1 つ添える。除外理由は次の語彙から選ぶ:

- 写経 / change-detector になる
- 取るに足らない
- 結合テスト / 手動確認に隔離
- 上流の純粋ロジックのテストで担保済み
- 既存テストがカバー済み

運用フロー

この markdown を用いて、ざっくり次のような流れで運用してみています。

  • Claude Code の plan mode を使用して plan を立てる。plan を立てる段階で、「どんな振る舞いをテストするつもりか/どんな振る舞いを確認しないと決めたか」を先に書き出させる
  • その一覧を人間がチェックし、過不足がなさそうかを確認してから実装に進ませる
  • 出来上がった plan を、他の AI(Codex など)にも test-design.md を物差しとして渡したうえでレビューさせる
  • Opus に実装させる
  • plan を実装させた後のレビュー時にも、テストが test-design.md に沿っているかを他 AI に確認してもらう

出力はどう変わったか

「振る舞い」の記述について

test-design.md を入れる前と後で、AI が出してくる「確認すべき振る舞い」の説明は、自分の体感ではだいぶ読みやすくなりました。いくつか例を挙げます。

  • 実装用語ではなく、外部から見た振る舞いで書くようになりました。
Before(実装・テスト用語で書いた、AI っぽい書き方):
機能固有 logic (*HogeError wrap + ctx 連携) を representative cases で pin

After(利用者から見た結果で言い切る書き方):
外部コマンドが異常終了したら、その終了コードを保ったままエラーを返す。
実行中に呼び出し側がキャンセルしたら、途中でも速やかに止まる。
  • 何をどう確認するのかを具体的に書くようになりました。
Before:
sampleDepths の boundary を網羅

After:
総件数が 0 / ちょうど 1 ページ分 / 大量、のどれであっても、最も浅いページと最も深いページを必ず含むようにする。
  • 体言止めせず、主語や条件を省略せずに動詞で言い切るようになりました。
Before:
時間切れに達すると以降の深度を叩かず CutoffHit=true、親キャンセルでは残りを叩かず CutoffHit=false

After:
時間切れ(cutoff)に達すると、以降のページは叩かず、時間切れだったと記録される。
強制キャンセル(Ctrl-C)では、残りのページを叩かず、時間切れではないと記録される。

テストコードの品質について

テストコードについても、読みやすさと品質がどちらも向上しました。
test-design.md を入れる前は、以下のようにメソッド名の対応表をテストにそのまま書き写しただけの、「誰かが値を変えたら落ちる」だけのテストがありました。

// 各画面が呼ぶべき API メソッド名を、対応表としてテストに固定する
func TestCatalogMethods(t *testing.T) {
want := map[string]string{
"user": "getUsers",
"order": "getOrders",
"product": "getProducts",
}
for screen, method := range want {
if CatalogFor(screen).Method != method {
t.Errorf("screen=%s method=%q, want %q", screen, CatalogFor(screen).Method, method)
}
}
}

test-design.md を入れたことで、AI がこの写経テストを検出し、「識別子が重複していない」等の、壊れていたら必ずバグと言える性質だけを確認するテストに書き換えてくれました。

// 壊れていたら必ずバグになる、カタログの構造的な性質だけを確認する
func TestCatalogWellFormed(t *testing.T) {
seen := map[string]bool{}
for _, e := range endpoints {
if seen[e.Slug] {
t.Errorf("slug が重複している: %q", e.Slug) // 重複すると画面の選択が壊れる
}
seen[e.Slug] = true
if e.Method == "" {
t.Errorf("%s: メソッド名が空", e.Slug)
}
}
// メソッド名そのものは照合しない。
// それはバックエンドとの契約で、結合テストでしか真偽を確かめられないため。
}

その他、この運用を試してみて感じたこと

「確認すべき振る舞い」を読んでいると、「これは自分が思っていたのと違う」と気づけるケースがそこそこありました。実装に入る前に認識のズレを潰せるのは嬉しいです。

また、「確認しないと決めた振る舞い」も一緒に出してもらうことで、「漏れているのではなく、意図して外したのだ」と分かり、一定の安心感が生まれました。

とはいえ、振る舞いの分量は依然として多く、確認するのはそれなりに大変です。「そもそも毎回律儀に全部の振る舞いを確認すべきなのか、ある程度は AI に任せてしまっても良いのではないか」というのは、別の論点としてあるなと思っています。

おわりに

AI を使えば、アプリケーションコードもテストコードも簡単に書けるようになりました。テストまで含めて勝手に生成される状況になると、つい楽な方に流れて、中身を確認しないまま取り込んでしまいがちです。

ですが、ソフトウェアに品質が求められる限り、その品質に責任を持つのは人間です。あるべき振る舞いを決める役割は、生成のスピードがどれだけ上がっても人間に残り続けると思っています。この役割が残る以上、「このソフトウェアはどう振る舞うべきで、そのためにどんなテストを書くべきか」を考えたり確かめたりする部分からは逃げずに、考えるべきなのではないでしょうか。

AI 時代におけるテスト品質の保ち方について、「自分のチームではこうしている」「ここはこう思う」といった意見があれば、ぜひコメント等いただけますと幸いです。