AI を使ったテストコード自動生成ハンズオン:既存コードに後からテストを足す

概要

「テストを書きたいが時間がない」「レガシーコードにテストが一本もない」という状態を、AI で崩すためのハンズオンです。LLM にテストを丸投げするのではなく、人間が仕様の番人をやり、AI に反復作業を肩代わりさせるという分担で進めます。

扱う流れは次のとおりです。

  1. テスト対象の読み込ませ方(コンテキストの渡し方)
  2. ケース設計を先に出させて人間がレビューする
  3. 実際のテストコードを生成させる
  4. 生成テストを信用しないための検証(カバレッジ・ミューテーション)
  5. CI への組み込み

言語は Python(pytest)を主に、TypeScript(Vitest)の例も添えます。

環境

  • Python 3.11 / pytest 8.x / pytest-cov / mutmut
  • Node.js 20 / Vitest 1.x
  • 任意の LLM(コード生成が可能なモデル。API/CLI いずれでも可)

掲載コードは手元の pytest / vitest で実行して通ることを確認した最小例です。

発生した問題:AI 任せのテストは「通るだけ」になりがち

AI に「このコードのテストを書いて」とだけ頼むと、よくこうなります。

  • 実装をそのままなぞったトートロジーなテスト(実装のバグごと追認する)
  • ハッピーパスだけで、境界値・異常系が抜ける
  • モックを過剰に積んで、結局何も検証していない
  • 存在しない関数やフィクスチャを呼ぶ(ハルシネーション)

「カバレッジは上がるのにバグは落ちる」という最悪の状態です。原因は、AI が実装を正解とみなしてしまうことにあります。

原因

テストの価値は「実装とは独立した仕様の表明」にあります。ところが実装コードだけを渡すと、AI はその実装の振る舞いを正としてアサーションを書く。実装が間違っていてもテストは緑になります。

鍵は 実装ではなく仕様を渡すこと、そして ケース設計を実装生成から分離することです。

対応方法

ステップ1:対象を決め、仕様を言語化する

割引計算というありがちなドメインロジックを例にします。

# pricing.py
def apply_discount(price: int, rate: float) -> int:
    """価格に割引率を適用して整数円で返す。

    - price は 0 以上の整数(円)
    - rate は 0.0〜1.0。範囲外は ValueError
    - 端数は切り捨て(floor)
    """
    if not (0.0 <= rate <= 1.0):
        raise ValueError("rate must be between 0.0 and 1.0")
    return int(price * (1 - rate))

docstring に仕様を書いておき、AI にはこの docstring を「正」として渡します。

ステップ2:まずケース表だけを出させる

いきなりコードを生成させず、「テストケースの一覧表」だけを出させます。

次の関数の docstring を仕様の正とみなし、pytest 用の
テストケースを表で列挙してください。コードはまだ書かないでください。
各ケースに「区分(正常/境界/異常)」「入力」「期待結果」「狙い」を書いてください。
境界値と異常系を優先してください。

出てきた表を人間がレビューします。

区分 入力 期待結果 狙い
正常 price=1000, rate=0.1 900 基本動作
境界 rate=0.0 price と同値 下限
境界 rate=1.0 0 上限
境界 price=0 0 価格ゼロ
境界 price=999, rate=0.1 899 端数 floor
異常 rate=-0.1 ValueError 下限外
異常 rate=1.5 ValueError 上限外

999 * 0.9 = 899.1 → 899 は端数切り捨てを突く良いケースです。抜けていれば人間が足します。仕様の抜けを埋めるのは人間の仕事です。

ステップ3:承認したケース表からテストコードを生成させる

# test_pricing.py
import pytest
from pricing import apply_discount


@pytest.mark.parametrize(
    "price, rate, expected",
    [
        (1000, 0.1, 900),
        (1000, 0.0, 1000),
        (1000, 1.0, 0),
        (0, 0.5, 0),
        (999, 0.1, 899),
    ],
)
def test_apply_discount_normal(price, rate, expected):
    assert apply_discount(price, rate) == expected


@pytest.mark.parametrize("rate", [-0.1, 1.5])
def test_apply_discount_invalid_rate(rate):
    with pytest.raises(ValueError):
        apply_discount(1000, rate)

ステップ4:TypeScript 版(Vitest)も同じ分担で

// pricing.ts
export function applyDiscount(price: number, rate: number): number {
  if (rate < 0 || rate > 1) {
    throw new Error("rate must be between 0 and 1");
  }
  return Math.floor(price * (1 - rate));
}
// pricing.test.ts
import { describe, it, expect } from "vitest";
import { applyDiscount } from "./pricing";

describe("applyDiscount", () => {
  it.each([
    [1000, 0.1, 900],
    [1000, 0, 1000],
    [1000, 1, 0],
    [0, 0.5, 0],
    [999, 0.1, 899],
  ])("applyDiscount(%i, %f) === %i", (price, rate, expected) => {
    expect(applyDiscount(price, rate)).toBe(expected);
  });

  it.each([-0.1, 1.5])("throws on invalid rate %f", (rate) => {
    expect(() => applyDiscount(1000, rate)).toThrow();
  });
});

確認方法

カバレッジを測る

pytest --cov=pricing --cov-report=term-missing

Missing に未到達行が出ます。これを AI にフィードバックして「この行を踏むケースを足して」と頼むループが有効です。ただしカバレッジ 100% は「全行が実行された」だけで「正しく検証された」の保証ではありません。

ミューテーションテストで「テストの手抜き」を暴く

実装をわざと壊して(- を + にする等)、テストが失敗するかを見ます。失敗しなければ、そのテストは意味をなしていません。

pip install mutmut
mutmut run --paths-to-mutate=pricing.py
mutmut results

survived(生き残った変異)が出たら、それを殺すケースを AI に生成させます。AI が書いたテストを AI 以外の指標で検品する——これがトートロジー問題への実務的な対策です。

注意点

  • 実装ではなく仕様を渡す。 仕様が無いなら、まず docstring やコメントに言語化する。これ自体が設計の棚卸しになる。
  • ケース設計と実装を分ける。 表を人間が承認してからコードを書かせると、抜けと冗長が激減する。
  • 生成されたフィクスチャ名・import を鵜呑みにしない。 存在しないものを呼ぶことがある。必ず実行して確認する。
  • 機密を渡さない。 本番の接続情報・顧客データ・環境変数を含むコードをそのまま貼らない。対象を最小のモジュールに切り出して渡す。
  • カバレッジを目的化しない。 ミューテーション生存率やレビューと組み合わせて初めて意味がある。

まとめ

AI によるテスト自動生成は「書かせる」より「設計を承認し、検品する」ワークフローにすると実用になります。

  1. 仕様を言語化して渡す
  2. ケース表を先に出させて人間がレビュー
  3. 承認後にコード生成
  4. カバレッジ+ミューテーションで検品
  5. 足りないケースをフィードバックしてループ

AI は反復と網羅の下書きが速く、人間は仕様の番人に専念できます。この分担が、テスト自動生成を実務で機能させる鍵です。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。