2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIでテストファースト開発すると実装の中身よりもテストケースを考えるようになった話

2
Last updated at Posted at 2026-02-13

はじめに

この記事では、AIを使ってテストファースト開発を続ける中で、実装中心だった思考がテストケース中心に変わっていった過程をまとめます。
あわせて、現場で回しやすい進め方や注意点も整理します。

AIを使ってテストファーストで開発していると、頭の使い方が少し変わってきます。
以前は「どう実装するか」を最初に考えていましたが、今は「何を満たせば正しいと言えるか」を先に考える時間が増えました。

これはAIが実装を補助してくれるからです。
実装の詳細より、テストケースの質がそのまま成果物の質に直結しやすくなりました。

実装より先にテストケースを考える理由

AIは実装案を高速に出せます。
ただし、指示が曖昧だとそれらしいコードを作るだけで、仕様を外すことがあります。

そこで重要になるのがテストケースです。

  • 正常系で何を保証するか
  • 境界値でどこまで許容するか
  • 異常系で何を返すか
  • 副作用をどこまで許すか

これを先に決めると、AIへの指示も評価も明確になります。

変わったのは「実装力」より「仕様分解力」

AI導入後に伸びたのは、アルゴリズムを書く速度より、仕様をテスト可能な単位へ分解する力でした。

例えば、次の観点を先に言語化するようになります。

  • 入力の前提条件
  • 出力の期待値
  • 例外時の振る舞い
  • データの不変条件

この分解ができると、AIが出した実装が多少違っても、テストで正否を判断できます。

導入前と導入後で変わった思考順序

以前は次のような流れでした。

  • まず実装を書く
  • 動かして問題が出たら直す
  • 最後にテストを足す

AIを使うようになってからは、次の流れに変わりました。

  • 先にテストケースを列挙する
  • AIに実装させる
  • テスト結果を見て仕様との差分を詰める

この違いで大きいのは、実装の巧さより、仕様の言語化の質が成果を左右する点です。

実際に起きた変化

テストファーストをAIと回すと、日々の開発で次の変化が起きやすいです。

  • 実装レビューよりテストケースレビューの比重が上がる
  • 先に失敗するテストを書くので、仕様の抜けに早く気づく
  • 実装の書き換えが怖くなくなる
  • バグ修正時に再発防止テストを追加する流れが自然になる

結果として、実装の中身そのものへの執着が少し減ります。
代わりに、仕様をどれだけ検証できているかに意識が向きます。

先にテストケースを作り切る進め方も有効

最近は、実装に入る前にテストケースを一度作り切ってしまう進め方も有効だと感じています。
特にエッジケースを先に洗い出しておくと、後からの手戻りが減ります。

  • 正常系を最小セットで定義する
  • 境界値と異常系を先に列挙する
  • 条件の組み合わせケースを先に作る
  • 仕様上あいまいなケースを先に議論して確定する
  • そのテストケース群をAIへの実装指示とレビュー基準に使う

この順番にすると、実装の速さより「何を満たせば完成か」が先に固まります。
特に組み合わせケースまでテストで示すと、自然言語だけで説明するよりAIが仕様を理解しやすくなります。

例えば hoge のケースでは、「閾値を超えたユーザーを1人だけアラート化する」のか「閾値を超えたユーザー全員をアラート化する」のかを、次のようにテストで固定できます。

func TestRunWithMultipleUsersAboveThreshold(t *testing.T) {
	t.Run("複数のユーザーが閾値を超えた場合は全てアラート化されること", func(t *testing.T) {
		loc, err := time.LoadLocation("Asia/Tokyo")
		if err != nil {
			t.Fatalf("failed to load location: %v", err)
		}

		clock := fixedClock{now: time.Date(2026, 1, 24, 10, 0, 0, 0, loc)}
		store := &fakeStore{
			totals: []WeeklyTotal{
				newTestWeeklyTotal("u1", 100_000),
				newTestWeeklyTotal("u2", 200_000),
				newTestWeeklyTotal("u3", 300_000),
				newTestWeeklyTotal("u4", 99_999), // 閾値未満
			},
		}
		svc := Service{Store: store, Clock: clock}
		params := RunParams{
			AlertType: AlertTypeTradeWeekly,
			UserType:  UserTypeIndividual,
			RiskClass: RiskClassMedium,
		}

		if err := svc.Run(context.Background(), params); err != nil {
			t.Fatalf("run failed: %v", err)
		}

		if got := len(store.inserted); got != 3 {
			t.Fatalf("expected 3 alerts, got %d", got)
		}

		gotUsers := make(map[string]struct{}, len(store.inserted))
		for _, alert := range store.inserted {
			gotUsers[alert.UserID] = struct{}{}
		}

		for _, uid := range []string{"u1", "u2", "u3"} {
			if _, ok := gotUsers[uid]; !ok {
				t.Errorf("expected alert for user %s, but not found", uid)
			}
		}
	})
}

この形にしておくと、実装が1件だけ通知してしまうのか、対象ユーザー全員を通知するのかという解釈ぶれを減らせます。

それでも一度では出し切れないので反復する

とはいえ、最初の1回でテストケースをすべて出し切るのは現実的ではありません。
まずは大まかなパターンを列挙し、実装を進めながらもう一度テストケースを見直して、欠けているケースを追加していく進め方が実務では安定します。

この反復を回すときは、次の順で進めると整理しやすいです。

  • テスト失敗を仕様差分として記録する
  • 差分が仕様不足か実装不具合かを切り分ける
  • 仕様不足ならテストケースを追加する
  • 実装不具合なら実装だけ直して再実行する

テストと実装を同時に育てる意識があると、途中で迷いにくくなります。

AIに渡すときのテストケースの書き方

ここは厳密にやりすぎると、準備コストが重くなります。
最近のAIは意図を汲む力が上がっているので、最初は雑な自然言語で渡しても十分回せる場面が多いです。

例えば次のような粒度でも、まずは実装とテストのたたき台を出せます。

  • hoge の条件では複数件を返してほしい
  • 通常は最新順で返す
  • 条件が足りない場合はエラーにする

まずは日本語でもよいので、ざっくり「作成してほしいテスト」をAIに渡すのがおすすめです。
その日本語をそのまま Go のテスト実行名にすると、仕様とテストコードの対応が取りやすくなります。
例えば t.Run("条件が足りない場合はエラーを返すこと", func(t *testing.T) { ... }) のようにしておくと、テスト名を読むだけで意図を確認できます。

この進め方で重要なのは、最初から完璧に書くことではなく、方向がずれたらすぐ止めて修正することです。
そのため、最初の指示は軽く、確認と修正を早く回すほうが実務では速いです。

一方で、複雑な仕様や不具合が続く箇所は、条件を明文化したほうが安定します。
その場合はテストケースを次の形式で整理します。

  • 前提条件
  • 入力
  • 期待値
  • 補足ルール

例としては次のような形です。

  • 前提条件: データは有効なユーザーのみ対象
  • 入力: status=active, limit=10
  • 期待値: 作成日時降順で最大10件
  • 補足ルール: hoge 条件が重なる場合は複数件返却可

このレベルまで明記すると、AIの実装と人間の期待値が揃いやすくなります。

DB層はモックしない前提が効く

DB層を基本モックしない運用は、AI活用時にも相性が良いです。
理由は、実データに近い挙動でテストできるため、仕様理解のズレを早めに拾えるからです。

  • SQLの条件漏れ
  • ソート順の違い
  • NULLや重複の扱い
  • 複数件返却時の順序と件数

このあたりは、モック中心では見落としやすい領域です。

注意点

テストファーストをAIで回すときは、次の落とし穴があります。

  • テスト自体が仕様を誤解している
  • 期待値の定義が曖昧で、テストが仕様の代わりになれていない
  • ケースの抜けがあるまま、テストが通ったことで安心してしまう

もうひとつ実務で起きやすいのが、見かけ上はテスト成功でも、内部ではエラーが発生しているケースです。
例えば、バッチ処理の成功だけを確認するシナリオテストでは、最終的なジョブ結果は成功でも、内部の登録処理で一時的なエラーや重複insertが起きていることがあります。
処理自体は完走するため見逃しやすいのですが、ログは汚れ続けるため、放置すると保守性が下がります。
こうした箇所は人間がログまで細かく確認すると、テストコードの品質を上げやすくなります。

そのため、次の観点を確認しておくと安全です。

  • テスト実行ログに握りつぶされた例外がないか
  • 初期データ作成で一意制約違反や重複insertが起きていないか
  • try-catch で広く例外を吸収していないか

対策はシンプルです。

  • 期待値の根拠を仕様書やユースケースに結びつける
  • DB層は基本モックせず、実データに近い条件で検証する
  • 契約テストや統合テストで現実との接点を持つ
  • テストコードも本体コードと同じ基準でレビューする

自分の体感では、AIがテストに合わせた不自然な実装を出すケースはそこまで多くありません。
それよりも、テストケースの設計が甘いまま進むことのほうが、実際の品質に効いてくると感じています。

細かく書くほど良いわけではない

ここで気をつけたいのは、テストケースを細かくしすぎると、内部実装に依存した壊れやすいテストを量産しやすい点です。
実装の手順や内部関数の呼ばれ方まで固定すると、仕様は変わっていないのにテストだけ壊れる状態になります。

大事なのは、アプリケーションとしての振る舞いを細かく検証することです。

  • 何を入力したら何が返るか
  • どの条件で成功/失敗するか
  • 副作用として何が保存・更新されるか
  • 利用者から見て観測できる結果が正しいか

逆に、次のような項目は必要以上に固定しないほうが安定します。

  • 内部の関数分割や呼び出し順
  • 中間オブジェクトの細かい形
  • 実装都合の一時的な処理手順

中の実装を意識しないテストに寄せるほど、リファクタリング耐性が上がり、AIとの反復でも保守しやすくなります。

チーム運用で意識していること

個人で回せても、チームで回せないと効果は限定的です。
そのため、次の運用ルールを置くと定着しやすくなります。

  • 実装PRと同じ重さでテストケースをレビューする
  • 新規バグには再発防止のテスト追加を必須にする
  • テスト名を仕様文として読める形にする
  • テスト名を見ながら、日本語で未作成のテストがないか確認する
  • ケース分類を定期的に整理して重複を減らす

テストケースをチームの共通言語にできると、AI活用の再現性も上がります。

まとめ

AIでテストファースト開発を始めると、実装の中身よりテストケースを先に考える時間が増えます。

これは手抜きではなく、品質保証の重心が移ったということです。
実装はAIが支援できる時代だからこそ、人間は「何を正解とするか」を定義する役割を強く持つようになります。

最初に完璧なテストケースを作る必要はありません。
大枠を作って反復し、エッジケースと組み合わせケースを追加していく運用が現実的です。

その意味で、これからの開発力は実装速度だけでなく、良いテストケースを設計し続ける力で決まると感じています。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?