Goで書くコード品質の高め方

品質モデルと検証の階層から、AIにコード生成を任せる際の品質メカニズムまで


Posted on 2026年 8月 18日 (火)
Tags golang, testing, e2e, observability, software-quality, ai-coding, cowork-with-llm
golang, testing, e2e, observability, software-quality, ai-coding, cowork-with-llm

Goで書くコード品質の高め方

「コード品質を上げる」という話は、たいてい lint とテストカバレッジから始まって、そこで終わる。だが網羅性を主張するには、その前に「品質とは何の集合か」を決めておく必要がある。定義がなければ、何が抜けているかも言えない。

この記事では、まずソフトウェア品質の学術的な定義(品質モデルの系譜と ISO/IEC 25010)を置き、そのうえで検証手段を「保証の強さ」で階層化する。単体テスト・E2Eテスト・Observability の代表的フレームワークはその階層の中に位置づける。さらに、テストコードを仕様書として運用する方法、テストコード自体の正しさをどう担保するか(オラクル問題)まで降りて、「これで品質は保証されるのか」という問いに原理的な限界の側から答える。

そしてこの記事の実際の狙いは §12 にある。AIにコード生成を任せたとき、どういう仕組みがあれば高い品質を実現できるのか、という問いだ。結論を先に書くと、AIは品質保証の限界を1つも解消しない。だが各検証層のコストを大きく変えるため、何を人間が持ち、どこへ投資すべきかの最適解が変わる。§1〜§11 はそれを論じるための道具立てになる。

品質が保証できるかという問いへの答えは、先に書いておくと No だ。それは手を抜いているからではなく、計算可能性と認識論のレベルで決まっている。到達できる最良の状態は「保証」ではなく、どのリスクをどの手段でどこまで下げたかを説明できる状態になる。

まとめ

  • ソフトウェア品質は単一の量ではない。ISO/IEC 25010:2023 は製品品質を9特性(機能適合性・性能効率性・互換性・相互作用性・信頼性・セキュリティ・保守性・柔軟性安全性)に分解する。テストとo11yが直接カバーするのはこのうち一部でしかない
  • 検証手段には保証の強さの階層がある。規約(lint)< 型 < テスト < 契約 < 抽象解釈・記号実行 < モデル検査 < 定理証明。上に行くほど強い保証が得られるが、コストが指数的に上がる
  • Rice の定理により、非自明な意味的性質はすべて決定不能。したがって静的解析は「健全(見逃さない)」と「完全(誤検出しない)」を両立できない。これは実装の努力で解決できる問題ではない
  • 単体テストの代表格は testing 標準パッケージ + testify、モックは go.uber.org/mock、プロパティベーステストは pgregory.net/rapid
  • E2E/統合テストは net/http/httptesttestcontainers-gogodog に加え、Go 1.25で正式化された testing/synctest(並行コードの決定論的テスト)が大きい
  • o11yは log/slog、OpenTelemetry Go SDK、prometheus/client_golang が事実上の標準。o11yは「事前の保証」であるテストに対する「事後の検証系」として位置づける
  • テストコードの仕様書化は、テーブル駆動テストの表をユースケース一覧にする、t.Run のサブテスト名を仕様文にする、ExampleXxx を godoc へ載る実行可能仕様にする、の3つが効く
  • テストの正しさの根本問題はオラクル問題(何が正解かを知らずに正誤は判定できない)。カバレッジはこれを解決しない(Inozemtseva & Holmes, ICSE 2014 はカバレッジとテストスイート有効性の相関が弱いことを示した)。有効なのはミューテーションテスト差分テストメタモルフィックテスト
  • **欠陥除去効率(DRE)**の観点では、単独で95%を超える検証手法は存在しない。層を重ねて初めて高いDREに届く。これが「1つの手法では足りない」ことの定量的な根拠になる
  • AIにコード生成を任せる場合(§12)、原理的な限界は1つも変わらないが各層のコストが変わる。ボトルネックは「書く」から「確かめる」へ移り、人間のレビュー速度は生成速度に追随できない
  • 実装とテストを同じモデルに書かせても検証層は増えない。DREの掛け算は各層の失敗の独立性を前提にするが、同一モデルなら誤解は両方へ相関して入る。「AIが書いてCIが緑」は1層であって2層ではない
  • AI下ではオラクルの価値が再配置される。明示オラクル(人が書く期待値)の価値は下がり、導出オラクル(差分・メタモルフィック)と暗黙オラクル(コンパイルエラー・型・-racegovulncheck)の価値が上がる。後者はモデルの仕様理解と完全に独立しているため
  • 同様に型とコンパイルの価値が上がる。型で表現した制約はモデルが黙って破れず、しかもエージェントへのフィードバックが最速になる
  • DORAの報告では、AI採用とデリバリ安定性の負の相関が一貫して観測されている。AIは増幅器で、統制系(自動テスト・バージョン管理・速いフィードバック)がある組織で効き、ない組織では不安定化を増幅する
  • 一文にすると: AIに実装を任せてよい。ただし「何が正しいか」の定義と、それを機械的に検査する仕組みは手放してはならない
  • 検証環境: Go 1.26.6(linux/amd64)。バージョン依存の記述はその都度明示する

1. そもそもソフトウェア品質とは何か

1.1 品質モデルの系譜

「品質」を工学的に扱うには、測定可能な特性へ分解する必要がある。この分解の枠組みを品質モデルと呼び、1970年代から系譜がある。

モデル内容
1977McCall モデル(McCall, Richards, Walters)11の品質因子を「運用」「改訂」「移行」の3視点で整理
1978Boehm モデル(Boehm et al.)効用の階層として品質を分解
1987FURPS(Grady & Caswell, HP)Functionality, Usability, Reliability, Performance, Supportability
1995Dromey モデル製品の構造的性質と品質属性を結びつける
1991/2001ISO/IEC 9126国際標準化。6特性
2011ISO/IEC 25010(SQuaRE)9126を置き換え。製品品質8特性+利用時の品質5特性
2023ISO/IEC 25010:2023改訂。製品品質9特性。Safety を追加、Usability→Interaction Capability、Portability→Flexibility

現在の到達点が ISO/IEC 25010:2023 になる。重要なのは、この標準が 品質を9つの直交する軸に分解している 点で、「テストが通る」はこのうち主に機能適合性と信頼性の一部にしか対応しない。

1.2 ISO/IEC 25010:2023 の9特性とGoでの検証手段

各特性に対して、Goで何を使えば検証できるかを対応させる。この表が、この記事の残りの地図になる。

品質特性意味Goでの主な検証・実現手段
機能適合性要求された機能を正しく満たすか単体テスト、E2Eテスト、godog、形式仕様
性能効率性時間・資源の効率testing.Bbenchstatpprof、負荷試験
互換性他システムとの共存・相互運用apidiffgorelease、契約テスト(pact-go)、Go 1互換性保証
相互作用性使いやすさ(旧ユーザビリティ)CLI/API設計レビュー、godoc の可読性、エラーメッセージ設計
信頼性故障せず動き続けるか障害注入、カオス実験、SLO、リトライ・冪等性の設計
セキュリティ機密性・完全性・真正性govulncheckgosec、Fuzzing、SBOM、sum.golang.org
保守性変更のしやすさメトリクス、アーキテクチャ適合性検査、テストの仕様書化
柔軟性環境変化への適応(旧移植性)ビルドタグ、GOOS/GOARCH、依存の抽象化、GODEBUG
安全性人命・財産・環境へのリスク回避ハザード分析(STPA)、フェイルセーフ設計、リミッタ

ここで既に分かること: テストとo11yを完璧にやっても、相互作用性・柔軟性・安全性はほぼ手つかずになる。「テストの通過=高い品質」と言えない理由は、感覚ではなく分解の問題にある。

1.3 製品品質と利用時の品質は別物

ISO/IEC 25010 は、製品そのものの品質(product quality)と、利用時の品質(quality in use、有効性・効率性・満足性・リスク回避性・利用状況網羅性)を分けている。前者が満点でも、後者が低い状況はいくらでもある。ユーザーが求めていない機能を完璧に実装した場合がそれにあたる。

Weinberg は品質を「誰かにとっての価値(value to some person)」と定義した。品質は製品に内在する絶対量ではなく、製品と利害関係者の関係として定まる。「誰にとっての品質か」を決めずに「品質を保証した」とは言えない、というのがこの定義の含意になる。

2. 検証手段には保証の強さの階層がある

2.1 Dijkstra の指摘

Program testing can be used to show the presence of bugs, but never to show their absence! (プログラムのテストはバグの存在を示せるが、不在を示すことは決してできない) — Dijkstra, Notes on Structured Programming (1970)

これは修辞ではなく、有限個の入力しか試せない手法の論理的な限界を述べている。テストは存在証明であって不在証明ではない。不在を示したければ、入力空間全体を覆う別の道具が要る。

2.2 階層の全体像

手法保証の対象強さコストGoでの手段
0規約・整形表層の一貫性最弱最小gofmtgolangci-lint
1型システム型に表現できる性質弱〜中小(言語に内蔵)Goの型、ジェネリクス、独自型
2テスト実行した入力に対する挙動中(存在証明のみ)testingtestify
3プロパティ/Fuzzing生成された広い入力集合中〜強rapidgo test -fuzz
4契約(表明)実行時の事前・事後条件パニック、invariant 検査
5抽象解釈・記号実行近似した全実行強(近似あり)staticcheckgosec
6モデル検査モデルの全状態空間強(モデル限定)TLA+、SPIN/Promela、Gomela
7定理証明仕様に対する完全な証明最強最大Coq/Iris(Goose、Perennial)

この階層が「網羅性」の骨格になる。 実務でどこまで登るかはリスクで決めるが、「テストしかやっていない」は階層の2段目で止まっているという意味になる。

2.3 Rice の定理 — 静的解析の原理的限界

Rice の定理(1953)は、「プログラムの非自明な意味的性質はすべて決定不能」と述べる。「このプログラムは必ず終了するか」「この変数は必ず非nilか」といった問いに、すべてのプログラムに対して正しく答えるアルゴリズムは存在しない。

したがって、あらゆる静的解析器は次のいずれかを選ばざるを得ない。

  • 健全(sound): 本物のバグを見逃さない。代わりに誤検出(false positive)が出る
  • 完全(complete): 誤検出を出さない。代わりに見逃し(false negative)が出る

Goの go vetstaticcheck は実用性を優先して後者寄り、つまり「確実に怪しいものだけ報告する」設計になっている。lintがすべて通ることはバグがないことを意味しないのは、ツールの未熟さではなく理論的な帰結だ。

この限界のもとで最大限を取るための理論的枠組みが、Cousot & Cousot の抽象解釈(POPL 1977)になる。実行状態を安全に近似した抽象領域で解析することで、健全性を保ちながら計算可能性を確保する。現代の本格的な静的解析器はこの枠組みの上に立つ。

2.4 型システムは軽量形式手法

型検査は、プログラムを実行せずに一定の性質を証明する。つまり型システムは軽量形式手法(lightweight formal methods)にあたる。

Goの型システムで表現できることと、できないことを整理する。

1
2
3
4
5
6
// 型で表現できる: 単位や意味の取り違えを防ぐ
type UserID string
type OrderID string

func FindUser(id UserID) (*User, error) { ... }
// FindUser(orderID) はコンパイルエラーになる
1
2
// 型で表現できない: 値の範囲、状態遷移の順序、nil可能性
type Percentage int // 0〜100 という制約は型に入らない

Goには篩型(refinement type)や依存型がないため、値の範囲や不変条件は型で保証できない。この空白を埋めるのが実行時の契約(表明)になる。

1
2
3
4
5
6
7
// 契約: 事前条件を実行時に検査する(Design by Contract の簡易版)
func NewPercentage(v int) Percentage {
	if v < 0 || v > 100 {
		panic(fmt.Sprintf("percentage out of range: %d", v))
	}
	return Percentage(v)
}

Meyer が Eiffel で提唱した Design by Contract は、事前条件・事後条件・不変条件を仕様として明示する手法で、その理論的基盤は Hoare 論理(An Axiomatic Basis for Computer Programming, CACM 1969)にある。Goに言語機能としての契約はないが、コンストラクタ関数での検査と、パッケージ内部の不変条件検査(assertInvariant() のような関数)で実務的には近いことができる。

設計上の要点: 型で表現できる制約は型に寄せる。型で表現できない制約は契約で実行時に検査する。契約で検査した不変条件は、テストで検証すべき対象から外せる(常に成り立つことがコード上で強制されるため)。

2.5 形式手法をGoで使う

階層の6〜7段目は「使わない」と決めることが多いが、選択肢としては存在する。

モデル検査(設計レベル)

TLA+ は設計そのものを検証する。AWS は分散システムの設計に TLA+ を適用し、テストでは見つけられなかった深刻な設計バグを複数発見したと報告している(Newcombe et al., How Amazon Web Services Uses Formal Methods, CACM 2015)。ここで重要なのは、検証しているのはコードではなく設計モデルという点になる。コードの前段でアルゴリズムの正しさを固める用途に向く。

モデル検査(Goコードレベル)

  • Gomela: Goの並行処理部分を Promela へ変換し、SPIN でモデル検査する
  • セッション型/挙動型: Lange, Ng, Toninho, Yoshida, Fencing off Go: Liveness and Safety for Channel-based Programming(POPL 2017)は、Goのチャネル通信からモデルを抽出し、デッドロック自由性などを検証する手法を示した

定理証明

  • Goose / Perennial: Goのサブセットを Coq へ変換し、Iris という並行分離論理の上で証明する。MIT の GoJournal(GoJournal: a verified, concurrent, crash-safe journaling system, OSDI 2021)は、この方法でクラッシュ安全なジャーナリングシステムを検証した実例になる

現実的な判断: 一般的なWebサービスで7段目まで登る合理性はほぼない。ただし、分散合意、ロックフリーなデータ構造、課金計算、状態機械のように「間違いのコストが極端に高く、かつ状態空間が組合せ的に爆発する」箇所へ限れば、TLA+ による設計検証の費用対効果は見合いうる。

3. 静的解析とコードメトリクス

3.1 Lint

Goは標準ツールチェーンに go vetgofmt/goimports を同梱する。実務ではこれに golangci-lint を重ねるのが定番になっている。

1
2
$ go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
$ golangci-lint run ./...
1
2
3
# 確認コマンド
$ golangci-lint --version
$ go vet ./...

代表的な内訳は次のとおり。

  • staticcheck: バグになりやすいパターン(無意味な比較、到達不能コード)を検出
  • errcheck: エラーの握りつぶしを検出
  • gosec: SQLインジェクション、ハードコードされた資格情報などを検出
  • unused: 未使用の識別子を検出
  • govet: ロック値のコピー、フォーマット文字列の不整合などを検出

注意点: golangci-lint はv1系とv2系で設定ファイルの形式が異なる。CIで固定バージョンにピン留めしないと、linterの追加により既存コードが突然警告まみれになる場合がある。

3.2 コードメトリクス — 学術的な系譜と限界

保守性を定量化する試みには長い歴史がある。

メトリクス出典内容Goでの計測
循環的複雑度McCabe, IEEE TSE 1976制御フローグラフの独立経路数gocyclo
Halstead メトリクスHalstead, 1977演算子・オペランドの出現数から難易度を導出専用ツールは少ない
認知的複雑度Campbell(SonarSource), 2018人間の読解負荷に寄せた複雑度gocognit
C&K メトリクスChidamber & Kemerer, IEEE TSE 1994結合度・凝集度(CBO, LCOM, RFC など)Goはクラス概念が薄く適用が限定的
Martin メトリクスR.C. Martin求心結合 Ca、遠心結合 Ce、不安定度 I=Ce/(Ca+Ce)、抽象度 A、主系列からの距離 Dgodago-arch-lint
1
2
3
4
5
$ go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
$ gocyclo -over 15 .

$ go install github.com/uudashr/gocognit/cmd/gocognit@latest
$ gocognit -over 20 .

パッケージ依存の構造は goda で見られる。

1
2
$ go install github.com/loov/goda@latest
$ goda graph ./... | dot -Tsvg -o deps.svg

正直な但し書き: メトリクスの予測力を過大評価しないほうがよい。Landman らは大規模Javaコーパスで、循環的複雑度がコード行数(SLOC)と強く相関し、行数を超える追加の予測力は限定的であることを示した(ICSME 2014)。つまり「複雑度が高い」は多くの場合「長い」と言い換えられる。メトリクスは発見の入口(どこを見るべきかの当たりをつける)としては有用だが、品質のゲートとして閾値運用すると、関数を機械的に分割して数値だけ下げる行動を誘発する。

3.3 アーキテクチャ適合性の検査

設計上の依存規則(「domain層はinfra層に依存してはならない」など)は、レビューの努力ではなく機械で守る。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# .go-arch-lint.yml の例
version: 3
components:
  domain: { in: internal/domain/** }
  infra:  { in: internal/infra/** }
deps:
  domain:
    mayDependOn: []   # domain はどこにも依存しない
  infra:
    mayDependOn: [domain]

これはJava圏の ArchUnit と同じ考え方で、設計判断をテスト可能な形へ落とす手段になる。設計は放置すると必ず腐るため、規則を実行可能にしておく価値は高い。

4. 単体テスト

4.1 標準の testing とテーブル駆動テスト

Goのテストは標準ライブラリだけで完結する。慣習としてテーブル駆動テストが広く使われる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func TestCalculateTotal(t *testing.T) {
	tests := []struct {
		name  string
		items []Item
		want  int
	}{
		{name: "注文が0件のとき、合計は0円になる", items: nil, want: 0},
		{name: "複数商品の価格が合算される", items: []Item{{Price: 100}, {Price: 200}}, want: 300},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			got := CalculateTotal(tt.items)
			if got != tt.want {
				t.Errorf("CalculateTotal() = %d, want %d", got, tt.want)
			}
		})
	}
}

このパターンが後述する「テストコードの仕様書化」の土台になる。

4.2 テスト設計技法 — 入力をどう選ぶか

「どんなテストケースを書くか」には体系がある。行き当たりばったりに書くと、抜けが属人化する。

技法内容適用例
同値分割入力空間を挙動が同じクラスへ分け、各クラスから代表を選ぶ年齢: 負値/0-17/18-64/65以上
境界値分析クラスの境界とその前後を狙う17, 18, 19 / 64, 65, 66
デシジョンテーブル条件の組合せと結果を表にする割引条件が複数ある課金ロジック
状態遷移テスト状態機械の遷移と不正遷移を網羅注文ステータスの遷移
組合せ/ペアワイズ全組合せではなく2因子間の組合せを網羅設定フラグが多数ある機能

境界値分析が経験的に効くのは、欠陥が境界に集中するからだ。同様に、ペアワイズ法が実務で成立する根拠として、Kuhn らは多数の実障害を分析し、大半の障害が少数(多くは1〜3個)のパラメータの相互作用で発生することを報告している(Software fault interactions and implications for software testing, IEEE TSE 2004)。全組合せを試さなくても、2〜3因子の組合せを網羅すればかなりの割合を捕捉できる、という主張の裏付けになっている。

4.3 カバレッジ規準の階層 — Goが提供していないもの

カバレッジには強さの階層がある。

規準内容強さ
ステートメント(行)各文が1回以上実行される最弱
ブランチ(分岐)各分岐の真・偽を通る
条件 / 判定条件複合条件の各項が真・偽を取る
MC/DC各条件が単独で結果を変えることを示す非常に強
パス全実行経路を通る最強(現実には爆発する)

MC/DC(Modified Condition/Decision Coverage)は、航空機搭載ソフトの認証規格 DO-178C の最高安全水準(Level A)で要求される規準になる。a && b || c のような式に対して、各条件が単独で判定結果を左右することを個別に示す必要がある。

一方、Goが標準で提供するのはステートメントカバレッジのみだ。-covermodeset(通ったか)、count(通った回数)、atomic(並行安全なcount)の3種だが、いずれも行単位で、ブランチカバレッジやMC/DCは提供されない。

1
2
$ go test -covermode=count -coverprofile=cover.out ./...
$ go tool cover -html=cover.out

つまり Goで「カバレッジ100%」と言うとき、それは階層の最下段を満たしただけになる。次のコードはステートメントカバレッジ100%を達成しながら、b が偽の場合を1度も試していない。

1
2
3
4
5
6
func f(a, b bool) int {
	if a && b {   // a=true, b=true の1ケースだけで「この行は通った」になる
		return 1
	}
	return 0      // a=false の1ケースで通る
}

Go 1.20以降は統合テストを含むバイナリのカバレッジ収集(go build -cover)が可能になっており、E2Eテストがどこを通ったかを測れる。ただし規準の弱さは変わらない。

4.4 アサーション・モックライブラリ

用途ライブラリ補足
アサーションstretchr/testify事実上の標準。require は失敗時に即座に停止
差分比較google/go-cmp構造体の差分を読みやすく出力。非公開フィールドの扱いを明示できる
モック生成go.uber.org/mockgomock の後継
モック生成(軽量)matryer/moq生成コードが素直で読みやすい
HTTPモックjarcoal/httpmockRoundTripper を差し替える
1
//go:generate go run go.uber.org/mock/mockgen -source=repository.go -destination=mock_repository.go -package=mypkg

注意点: golang/mock は2023年にメンテナンスが go.uber.org/mock へ移管された。新規プロジェクトで前者を選ぶと更新の止まったライブラリへ依存してしまう。

モックの使いすぎに関する注意: モックで固めたテストは「実装がその順序でその関数を呼ぶこと」を検証しているだけの状態に陥りやすい。これは仕様ではなく実装の写像で、リファクタリングのたびに壊れる。境界(外部API、DB)はモックし、内部ロジックは実物を使う、という線引きが現実的になる。

4.5 プロパティベーステストとFuzzing

例示ベースのテストは、書いた人が想定した入力しか試さない。この構造的な弱点を埋めるのが次の2つになる。

プロパティベーステストは、Claessen & Hughes の QuickCheck(ICFP 2000)を起源とする。個別の期待値ではなく「任意の入力に対して常に成り立つ性質」を書く。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func TestEncodeDecodeRoundTrip(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		original := rapid.SliceOf(rapid.Int()).Draw(t, "input")
		encoded := Encode(original)
		decoded, err := Decode(encoded)
		if err != nil {
			t.Fatalf("Decode failed: %v", err)
		}
		if !slices.Equal(original, decoded) {
			t.Fatalf("round trip mismatch: got %v, want %v", decoded, original)
		}
	})
}

この書き方の本質的な利点は、入力を増やせることではなく、期待値を再実装しないで済む点にある。「エンコードしてデコードすると元に戻る」という性質は、実装を写し取らずに正しさを表現している。後述するオラクル問題への有力な対処となる。

代表的な性質の型は次のとおり。

  • 往復性: decode(encode(x)) == x
  • 不変条件: ソート後も要素の多重集合は変わらない
  • 冪等性: normalize(normalize(x)) == normalize(x)
  • 交換法則・結合法則: 順序を変えても結果が同じ
  • 参照実装との一致: 最適化版と素朴版の結果が一致する

Fuzzing は Go 1.18 から標準ツールチェーンに統合されている。カバレッジ誘導型で、実行経路を広げる入力を自動的に探索する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func FuzzParse(f *testing.F) {
	f.Add("2026-08-18")
	f.Fuzz(func(t *testing.T, s string) {
		v, err := Parse(s)
		if err != nil {
			return // 不正入力でエラーになるのは正常
		}
		// パースできたなら、再フォーマットして再パースすると一致するはず
		if v2, err := Parse(v.String()); err != nil || v2 != v {
			t.Fatalf("round trip failed for %q", s)
		}
	})
}
1
$ go test -fuzz=FuzzParse -fuzztime=30s ./...

注意点: Fuzzingのコーパスは testdata/fuzz 配下に蓄積されるためリポジトリが膨らむ。CIで毎回長時間回すのはコストが高いので、通常のCIでは既存コーパスの回帰実行に留め、専用ジョブで長時間走らせる運用が現実的になる。

5. 結合・E2E・システムテスト

5.1 代表的フレームワーク

目的ツール特徴
HTTPハンドラのテスト標準 net/http/httptestプロセス内で完結し高速
外部依存込みの統合テストtestcontainers-goDockerでDB・Redis・Kafkaを起動し実物を相手にする
BDD/仕様駆動cucumber/godogGherkin記法でシナリオを書く
ブラウザE2Eplaywright-community/playwright-goフロントエンドを含むE2E
API契約テストpact-foundation/pact-goConsumer-Driven Contract。サービス間の互換性を検証
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ctx := context.Background()
container, err := postgres.Run(ctx, "postgres:16-alpine",
	postgres.WithDatabase("test"),
	postgres.WithUsername("test"),
	postgres.WithPassword("test"),
)
if err != nil {
	t.Fatalf("start container: %v", err)
}
t.Cleanup(func() { container.Terminate(ctx) })

契約テストの位置づけ: マイクロサービス構成では、全サービスを起動するE2Eは遅く壊れやすい。Consumer-Driven Contract は、消費側が期待する契約を記録し、提供側がそれを満たすか個別に検証する。E2Eの網羅性を、独立に実行できる形へ分解する手法になる。

注意点: testcontainers-go はDockerデーモンへのアクセスを要求する。CI環境によっては Docker-in-Docker やソケットのマウント設定が必要になる。ローカルとCIで挙動が違う場合は docker info から疑う。

5.2 testing/synctest — 並行コードの決定論的テスト

並行処理のテストは長らくGoの弱点だった。タイマーを含むコードのテストは time.Sleep で待つしかなく、遅く、かつ環境負荷でフレーキーになる。

Go 1.24で試験導入され、Go 1.25で正式化された testing/synctest がこれを解決する。バブルと呼ばれる隔離環境の中で、time パッケージが偽のクロックになり、バブル内の全goroutineがブロックされたときだけ時計が進む。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func TestCacheExpiry(t *testing.T) {
	synctest.Test(t, func(t *testing.T) {
		c := NewCache(100 * time.Millisecond)
		c.Set("k", "v")

		time.Sleep(50 * time.Millisecond) // 実時間は経過しない
		if _, ok := c.Get("k"); !ok {
			t.Error("有効期限内なのに値が消えている")
		}

		time.Sleep(60 * time.Millisecond) // 合計110ms相当
		if _, ok := c.Get("k"); ok {
			t.Error("有効期限切れなのに値が残っている")
		}
	})
}

このテストは即座に完了し、かつ決定論的になる。synctest.Wait() はバブル内の全goroutineが持続的にブロックされるまで待つため、「この時点でまだ何も読まれていないこと」のような中間状態の検証もできる。

1
$ go doc testing/synctest

タイムアウト、リトライ、レート制限、ハートビートといった時間依存のロジックは、これまでテストが書きにくく品質の穴になりやすかった領域だ。ここが埋まった意味は大きい。

注意点: バブル内では外部との通信や、バブル外で起動したgoroutineとのやり取りを避ける必要がある。ネットワークを使う場合は偽の実装へ差し替える。

5.3 決定論的シミュレーションテスト(DST)

分散システムの障害は、タイミング・順序・部分故障の組合せで発生する。ランダムに走らせても再現できず、再現できなければ直せない。

決定論的シミュレーションテストは、乱数シード・スケジューリング・時刻・I/Oのすべてを決定論的に制御し、シードを記録すれば障害を完全再現できるようにする手法だ。FoundationDB が代表例で、シミュレータ上で数万年相当の実行を圧縮して回しながら、障害を注入し続ける設計を採る(FoundationDB: A Distributed Unbundled Transactional Key Value Store, SIGMOD 2021)。TigerBeetle や Antithesis も同系統の考え方を採る。

Goでは testing/synctest が単一プロセス内の決定論を提供する。プロセスをまたぐ範囲は、I/Oをインターフェース越しに抽象化し、シミュレータ実装へ差し替える設計を自前で用意することになる。

5.4 障害注入とカオスエンジニアリング

テストは「想定した障害」しか試せない。カオスエンジニアリングは、本番に近い環境へ意図的に障害を注入し、システムが定常状態を維持できるかを実験的に検証する手法になる(Basiri et al., Chaos Engineering, IEEE Software 2016)。仮説を立て、注入し、定常状態の指標が崩れないかを見る、という科学実験の形式を取る点が要になる。

手段ツール対象
ネットワーク劣化Shopify/toxiproxy遅延、帯域制限、切断
コード内の障害点pingcap/failpoint任意の箇所でエラー・パニックを注入
Kubernetes上の障害Chaos MeshPod kill、ネットワーク分断、I/O遅延

5.5 線形化可能性の検査

並行データ構造や分散ストアの正しさは「線形化可能性」で定義される。これは目視では検証できない。

  • anishathalye/porcupine: Go製の線形化可能性チェッカ。操作の履歴を記録し、逐次実行として辻褄の合う順序が存在するかを判定する
  • Jepsen / Elle: 分散データベースの分離レベル異常を、実行履歴から推論する(Kingsbury & Alvaro, Elle: Inferring Isolation Anomalies from Experimental Observations, VLDB 2020)

ここでのポイントは、正しさの判定を人間の目視から機械的な判定器へ移していることにある。これは次章のオラクル問題への直接的な回答の1つになる。

6. テストコードを仕様書として運用する

コードを読まなくても仕様が分かる状態を作るには、テストの書き方自体を仕様書として最適化する必要がある。動けばよいテストと、仕様書として読めるテストは別物になる。

6.1 仕様の形式度には階層がある

形式度表現実行可能性曖昧さ
自然言語の設計書Markdown、Wikiなし
構造化自然言語Gherkin(Given-When-Then)間接的
実行可能な例ExampleXxx、テーブル駆動テストあり
契約事前・事後条件あり(実行時)
形式仕様TLA+、Alloyあり(検証時)極小

乖離が起きるのは実行可能性がない層だけになる。自然言語の設計書は誰も更新しなくても壊れないが、実行可能な仕様はコードと矛盾した瞬間にCIが落ちる。これが「テストを仕様書にする」ことの本質的な利点だ。

6.2 サブテスト名を仕様文にする

t.Run の第一引数はテスト結果にそのまま出る。ここを実装の説明ではなく仕様として書く。

1
2
3
4
5
// 悪い例: 実装の説明になっている
t.Run("returns error when input is nil", func(t *testing.T) { ... })

// 良い例: 仕様として読める
t.Run("注文が0件のとき、合計金額は0円になる", func(t *testing.T) { ... })

出力がそのまま仕様一覧になる。

1
$ go test -v -run TestCalculateTotal ./...
1
2
3
4
=== RUN   TestCalculateTotal/注文が0件のとき、合計金額は0円になる
--- PASS: TestCalculateTotal/注文が0件のとき、合計金額は0円になる (0.00s)
=== RUN   TestCalculateTotal/割引クーポンは合計金額の10%を上限に適用される
--- PASS: TestCalculateTotal/割引クーポンは合計金額の10%を上限に適用される (0.00s)

6.3 テーブルをユースケース一覧にする

テーブル駆動テストの tests スライスは、そのまま「入力と期待結果の一覧表」になる。表の各行が1つの仕様で、新しい仕様の追加が1行の追加で済む設計にしておくと、実装を読まずに関数の責務が分かる。

このとき、テーブルに書く期待値は計算式ではなくリテラルにするのが要点になる。

1
2
3
4
5
// 悪い例: 期待値が実装を再現している(実装のバグがテストにも入る)
{name: "10%割引", price: 1000, want: 1000 - 1000*10/100},

// 良い例: 仕様として決めた値をそのまま書く
{name: "1000円の商品に10%割引を適用すると900円になる", price: 1000, want: 900},

6.4 ExampleXxx を実行可能なドキュメントにする

godoc に掲載され、かつ go test で実行もされる特殊な関数がある。

1
2
3
4
5
func ExampleCalculateTotal() {
	total := CalculateTotal([]Item{{Price: 100}, {Price: 200}})
	fmt.Println(total)
	// Output: 300
}

// Output: と実際の標準出力が一致しないとテストが失敗するため、ドキュメントとコードが乖離した瞬間にCIが落ちるpkg.go.dev 上でもサンプルコードとして表示される。コメントで書いた説明がコードと同期しなくなる古典的な問題を、実行可能な形で解決する仕組みになる。

6.5 BDDフレームワーク

godog(Gherkin)や Ginkgo + Gomega は、テストの構造自体を自然言語に近づける。Adzic の Specification by Example(2011)が説く「生きたドキュメント」の実装形になる。

1
2
3
4
5
Feature: 注文の合計金額計算
  Scenario: クーポンが適用されているとき
    Given 単価1000円の商品を3個カートに入れる
    When 10%割引クーポンを適用する
    Then 合計金額は2700円になる

トレードオフ: 自然言語へ近づくほど、Goの型検査やIDEの補完から離れる。QAなどGo以外のメンバーがシナリオを書くなら godog を選ぶ。Goエンジニアだけで完結するなら、標準の testing とサブテスト名の工夫で足りる場合は多い。読者に合わせて選ぶ。

6.6 仕様書化が成立しなくなる条件

次の状態になると、テストは仕様書として機能しなくなる。

  • テストが実装の写像になっている: モックの呼び出し順序を検証するテストは、実装を書き写しただけで仕様を語らない
  • セットアップが巨大: 前提条件を100行も並べると、何を検証しているのか読み取れない
  • 期待値が計算式: 6.3で述べたとおり、仕様ではなく実装の再現になる
  • テスト名が実装用語: TestHandlerReturns500 は仕様ではなく実装の記述になる

7. テストコード自体の正しさをどう保証するか

ここが最も見落とされやすい論点になる。テストがグリーンであることは、実装が正しいことの証明にならない。テスト自体に誤りがあれば、壊れた実装が通る

7.1 根本問題としてのオラクル問題

テストは「入力を与えて出力が正しいか判定する」が、この判定を下す仕組みをテストオラクルと呼ぶ。オラクルが手に入らない、あるいは正しくない場合、テストは何も保証しない。これがオラクル問題で、テスト研究の中心的な難問として体系的に整理されている(Barr, Harman, McMinn, Shahbaz, Yoo, The Oracle Problem in Software Testing: A Survey, IEEE TSE 2015)。

オラクルは大きく4種に分類できる。テストの正しさを高めるとは、弱いオラクルを強いオラクルへ置き換えることに他ならない。

種類内容Goでの例弱点
明示オラクル期待値を人が書くwant: 300人の理解が間違っていれば一緒に間違う
導出オラクル別経路の結果と比較する参照実装との差分テスト、往復性比較対象が必要
暗黙オラクル普遍的に異常な状態を検出パニック、-race 検出、nil 参照「異常でない誤り」は見つからない
人間オラクル人が目視で判断探索的テストスケールしない

明示オラクルの構造的弱点が最も重要になる。実装者とテスト作成者が同一人物なら、仕様の誤解は実装とテストの両方へ同じ形で入る。テストは実装と独立していないため、誤りが相関する。これは Knight & Leveson が多版プログラミングの実験で示した現象(独立に開発した版でも故障は相関する。IEEE TSE 1986)と同じ構図になる。

7.2 カバレッジは正しさの証明ではない

1
2
$ go test -coverprofile=cover.out ./...
$ go tool cover -func=cover.out | tail -1

カバレッジは「そのコードが実行されたか」を示すだけで、「結果が正しいと検証されたか」は示さない。次のテストは100%のステートメントカバレッジを達成しながら、何も検証していない。

1
2
3
func TestCalculateTotal(t *testing.T) {
	CalculateTotal(items) // 呼んでいるだけでアサーションがない
}

これは極端な例に見えるが、経験的にもカバレッジは有効性の指標として弱い。Inozemtseva & Holmes は大規模なJavaプロジェクトで、テストスイートのサイズを統制するとカバレッジとテストスイートの欠陥検出能力の相関は弱い〜中程度に留まることを示した(Coverage Is Not Strongly Correlated with Test Suite Effectiveness, ICSE 2014)。

したがってカバレッジの実務的な使い方は次になる。

  • 使ってよい: カバレッジレポートを見て、意図せず全く触れていない領域を発見する
  • 使うべきでない: カバレッジを目標値としてCIのゲートにする(通すためだけのテストを誘発する)

7.3 ミューテーションテスト — テストの検出力を測る

ミューテーションテストは、実装へ意図的に小さな変異(>>= に、+- に、条件を反転など)を注入し、既存のテストがそれを検出して失敗するかを機械的に確認する。検出できなかった変異(生存ミュータント)は、そこにテストの穴があることを直接示す。

理論的基盤は DeMillo, Lipton, Sayward(Hints on Test Data Selection, IEEE Computer 1978)で、2つの仮説に立つ。

  • 有能なプログラマ仮説: 実際の欠陥は正解に近い小さな誤りである場合が多い
  • 結合効果: 単純な変異を検出できるテストは、複雑な欠陥も検出する傾向がある

その後の実証研究も、この手法の有効性を支持している。Papadakis らは、ミューテーションスコアが実欠陥の検出能力とカバレッジより強く相関することを報告した(ICSE 2018)。Google は自社の大規模コードベースへミューテーションテストを組み込み、生存ミュータントをコードレビュー時に提示する運用を報告している(Petrović & Ivanković, State of Mutation Testing at Google, ICSE-SEIP 2018)。

Goでは gremlins が現状もっとも扱いやすい。

1
2
$ go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
$ gremlins unleash ./...
1
2
3
4
5
6
7
KILLED   CONDITIONALS_BOUNDARY  internal/order/total.go:42:15
LIVED    ARITHMETIC_BASE        internal/order/total.go:57:22
...
Mutation testing completed in 42s
Killed: 128, Lived: 14, Not covered: 3
Test efficacy: 90.14%
Mutator coverage: 97.93%

LIVED の行が「テストが守っていない箇所」を具体的なファイルと行で指す。カバレッジ率と違い、改善すべき対象が一意に定まるのが実務上の利点になる。

注意点: ミューテーションテストは変異ごとにテストスイートを再実行するため、実行時間が非常に長い。全体を毎回回すのは非現実的で、Googleの運用のように変更差分に絞って適用するのが現実的になる。また、ツールの選択肢は gremlinsgtramontina/oozego-mutesting 系のフォークがあるが、いずれもメンテナンス状況が変動するため、導入時点で最新の状態を確認したほうがよい。

7.4 差分テスト — 参照実装をオラクルにする

期待値を人が書かずに済ませる最も強力な方法が差分テスト(differential testing)になる。同じ入力を2つの独立した実装へ与え、結果が一致するかを見る(McKeeman, Differential Testing for Software, 1998)。Csmith はこの手法で複数のCコンパイラから多数のバグを発見した(Yang, Chen, Eide, Regehr, PLDI 2011)。

Goでの適用場面は次のようになる。

1
2
3
4
5
6
7
8
9
// 最適化した実装と、素朴だが明らかに正しい実装を突き合わせる
func TestFastSumMatchesNaive(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		xs := rapid.SliceOf(rapid.Int64Range(-1000, 1000)).Draw(t, "xs")
		if got, want := FastSum(xs), naiveSum(xs); got != want {
			t.Fatalf("FastSum = %d, naiveSum = %d, input = %v", got, want, xs)
		}
	})
}
  • 標準ライブラリの実装と自作の高速版を比較する
  • 移行前後の実装を並走させ、結果の差分を記録する(シャドウ実行)
  • 別言語で書かれた既存実装と突き合わせる

7.5 メタモルフィックテスト — 正解を知らずに検証する

正解が分からない場合でも、入力を変換したときの出力の関係なら分かることがある。これがメタモルフィックテストで、オラクル問題への代表的な対処になる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 検索スコアの正解値は分からないが、関係は分かる
func TestSearchMetamorphic(t *testing.T) {
	// 関係1: 検索語を絞り込めば、結果は元の結果の部分集合になるはず
	broad := Search("golang")
	narrow := Search("golang testing")
	for _, doc := range narrow {
		if !contains(broad, doc) {
			t.Errorf("絞り込んだ検索の結果 %v が、広い検索の結果に含まれていない", doc)
		}
	}
}

代表的なメタモルフィック関係には、入力の順序を変えても結果が同じ(順序不変性)、入力を拡大すると出力が単調に変化する(単調性)、無関係な要素を加えても結果が変わらない(無関連性)などがある。機械学習モデルや検索・推薦のように厳密な期待値を書けない領域で特に有効になる。

7.6 フレーキーテストはシグナルを壊す

不安定に成功・失敗するテストは、単に鬱陶しいだけでなくテストスイート全体の信頼性を破壊する。落ちても「またフレーキーだろう」と再実行されるようになると、本物の回帰を見逃す。

Luo らはフレーキーテストの原因を実証的に分類し、非同期待ち、テスト間の順序依存、並行性、外部リソースが主要因であることを示した(An Empirical Analysis of Flaky Tests, FSE 2014)。Googleも大規模環境で、テスト実行のうち無視できない割合がフレーキーであり、失敗の多くがフレーキーに起因することを報告している。

Goでの対処は具体的に決まる。

1
2
3
4
5
6
7
8
# 同一テストを繰り返し、不安定さを炙り出す
$ go test -count=20 -run TestSuspicious ./...

# データ競合の検出
$ go test -race ./...

# 並列実行時の順序依存を炙り出す
$ go test -shuffle=on ./...
  • 非同期待ちtime.Sleep をやめ、testing/synctest か明示的な同期に置き換える
  • 順序依存go test -shuffle=on で検出し、グローバル状態を排除する
  • 並行性-race を定期的に回す
  • 外部リソースtestcontainers-go でテストごとに隔離する

注意点: -race は実行時間とメモリのオーバーヘッドが大きい(数倍になる場合がある)。全テストで常用すると遅いため、CIでは専用ジョブへ分離する選択肢がある。

7.7 テストコードのlintとテストスメル

テストコード自体にも欠陥は入る。専用の検査で拾える典型がある。

  • testifylint: testify の誤用(assert.Equal の引数順序、assert.Nilassert.NoError の混同など)を検出
  • paralleltest / tparallel: t.Parallel() の付け忘れ・付けすぎを検出
  • thelper: テストヘルパーでの t.Helper() 呼び忘れを検出

テストスメルは van Deursen らが整理した概念で(Refactoring Test Code, 2001)、代表例は次になる。

  • Assertion Roulette: 1つのテストに多数のアサーションがあり、どれで落ちたか分からない
  • Eager Test: 1つのテストが複数の機能を検証しており、何の仕様か読めない
  • Mystery Guest: テストが外部ファイルやDBの状態へ暗黙に依存している

7.8 TDDの赤→緑サイクル

最も原始的だが確実な方法は、実装より先にテストを書き、期待どおり失敗することを一度目視することになる。この順序を踏むと、「実装を書く前からテストが成功していた」という不備(タイポでアサーションが無効化されている、対象が呼ばれていない)を機械的に排除できる。

既存コードへ後からテストを足す場合でも、実装を一時的に壊して、そのテストが確かに落ちることを確認する習慣は、コストが極めて低いわりに効果が高い。ミューテーションテストは、この確認を機械化・網羅化したものと理解するとよい。

8. Observability — 事後の検証系

8.1 位置づけ

テストは想定したケースしか検証できない。o11yは、本番で実際に何が起きているかを観測し、テストの想定外を事後に捕捉する。テストが「事前の保証」なら、o11yは「事後の検証」で、両者は代替ではなく補完になる。

なお「可観測性(observability)」は制御理論の用語(Kalman, 1960)で、システムの外部出力から内部状態を推定できる度合いを指す。ログ・メトリクス・トレースを出すこと自体が目的ではなく、外部から内部状態を復元できるかが判断基準になる。

8.2 代表的ライブラリ

領域ライブラリ補足
構造化ログ標準 log/slog(Go 1.21以降)構造化ログが標準へ入った。以前は zapzerolog が定番
メトリクスprometheus/client_golangPrometheus形式の事実上の標準
トレース・メトリクス統合OpenTelemetry Go SDK業界標準。エクスポータを差し替えて送信先を選べる
プロファイリング標準 net/http/pprof本番からCPU/メモリプロファイルを取得
1
2
3
4
5
6
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("request handled",
	slog.String("method", r.Method),
	slog.Int("status", status),
	slog.Duration("elapsed", time.Since(start)),
)
1
2
3
tracer := otel.Tracer("myservice")
ctx, span := tracer.Start(ctx, "ProcessOrder")
defer span.End()

8.3 何を観測するか — 確立された枠組み

出すべき指標には定石がある。

  • Four Golden Signals(Google SRE): レイテンシ、トラフィック、エラー、飽和度
  • RED メソッド(Wilkie): Rate、Errors、Duration。リクエスト駆動のサービス向け
  • USE メソッド(Gregg): Utilization、Saturation、Errors。資源向け

8.4 SLI / SLO / エラーバジェット

品質目標を数値で定義し、その達成度で意思決定する枠組みになる(Google SRE book, Beyer et al. 2016)。

  • SLI: 実測する指標(例: 成功したリクエストの割合)
  • SLO: 目標値(例: 30日間で99.9%)
  • エラーバジェット: 1 − SLO。許容される失敗の量

エラーバジェットが残っていれば機能開発を優先し、使い切ったら信頼性の改善へ切り替える、という明示的な意思決定ルールにできるのが要点になる。「品質を上げるべきか、機能を出すべきか」という終わらない議論を、数値で決着させる仕組みだ。

8.5 本番でのテスト

すべてを事前に検証しきれない以上、本番での検証を設計に組み込むほうが合理的になる。

  • カナリアリリース: 一部トラフィックへ先に流し、指標を比較して判断する
  • フィーチャーフラグ: デプロイと機能公開を分離し、問題時にコードを戻さず切り戻す
  • シャドウトラフィック: 本番トラフィックを新実装へ複製して流し、結果を比較する(7.4の差分テストの本番版)

注意点: メトリクスのラベルへユーザーIDやリクエストIDのような高カーディナリティな値を入れると、時系列DBのメモリ消費が急増する(カーディナリティ爆発)。ラベル設計はメトリクス追加時に必ずレビューする。高カーディナリティな情報はトレースかログへ載せる。

9. テストで測れない品質特性

ISO/IEC 25010 の9特性のうち、機能適合性以外は通常の単体テストでは測れない。ここを埋めないと網羅性は主張できない。

9.1 性能効率性

1
2
3
4
5
6
7
func BenchmarkCalculateTotal(b *testing.B) {
	items := makeItems(1000) // b.Loop() が初回呼び出しでタイマーを張り直すため ResetTimer は不要
	b.ReportAllocs()
	for b.Loop() {
		_ = CalculateTotal(items)
	}
}
1
2
3
$ go test -bench=. -benchmem -count=10 ./... > new.txt
$ go install golang.org/x/perf/cmd/benchstat@latest
$ benchstat old.txt new.txt

重要な点: ベンチマークを1回走らせて比較するのは統計的に無意味になる。benchstat は複数回の測定に対して統計的仮説検定(Mann-Whitney U 検定)を行い、差が有意かどうかとその信頼区間を出す。-count=10 以上を付けて比較するのが前提になる。

性能に関する理論的な道具も押さえておくとよい。

  • リトルの法則: 平均同時実行数 = 到着率 × 平均応答時間。必要な並行度の見積もりに使う
  • 万能スケーラビリティ則(Gunther): 並列化には競合とコヒーレンスのコストがあり、ある点を超えるとスループットが下がる。無制限にgoroutineを増やす設計への反証になる
  • コーディネーテッド・オミッション(Tene): 負荷試験ツールが遅延したリクエストの計測を落とすと、テールレイテンシを大幅に過小評価する。パーセンタイルの解釈時に注意が要る

9.2 セキュリティ

1
2
$ go install golang.org/x/vuln/cmd/govulncheck@latest
$ govulncheck ./...

govulncheck の特筆すべき点は、単に依存バージョンをCVEデータベースと突き合わせるのではなく、呼び出しグラフを解析して脆弱な関数へ実際に到達可能かを判定することにある。到達しない脆弱性のノイズが減るため、対応の優先順位を付けやすい。

サプライチェーンの側面も品質に含まれる。

  • sum.golang.org: Goのモジュールチェックサムデータベース。Certificate Transparency と同系のMerkle木による透明性ログとして構成されており、改竄の検出可能性が暗号的に保証される
  • SBOM: SPDX / CycloneDX 形式で依存を列挙する。cyclonedx-gomod などで生成できる
  • SLSA: ビルド由来の完全性を保証するフレームワーク。ビルドの再現性、来歴(provenance)の署名
  • 署名: Sigstore / cosign による成果物への署名
1
2
# 再現可能なビルドに近づける(絶対パスの埋め込みを避ける)
$ go build -trimpath -ldflags="-s -w" ./cmd/app

9.3 互換性と柔軟性

Go 1互換性保証 は、既存の正しいプログラムがコンパイルできなくならないという言語レベルの約束になる。Go 1.21以降は GODEBUG により、互換性に影響する挙動変更を個別に旧挙動へ戻せる仕組みが整備されている。

自作ライブラリの後方互換性は機械で検査できる。

1
2
3
# 公開APIの差分を検出する
$ go install golang.org/x/exp/cmd/apidiff@latest
$ apidiff old.api new.api
1
2
3
# セマンティックバージョニング的に妥当かを判定する
$ go install golang.org/x/exp/cmd/gorelease@latest
$ gorelease -base=v1.2.0

Goモジュールの semantic import versioning(v2以降はインポートパスにメジャーバージョンを含める)は、破壊的変更を型システムのレベルで別物として扱う設計になっている。同一プログラム内でv1とv2が共存できるのはこのためだ。

9.4 保守性と技術的負債

技術的負債は Cunningham のメタファ(OOPSLA 1992)に由来する。要点は「負債そのものが悪ではなく、利息を払い続ける自覚があるか」にある。SQALE のような手法は、負債を「修正に要する工数」として金額・時間へ換算し、可視化・優先順位付けを可能にする。

実務では、負債を測るよりその場所を特定するほうが有用になる。欠陥は一様に分布しない。

  • 欠陥の偏在: Ostrand & Weyuker らは、少数のモジュールへ欠陥が集中する(パレート的分布)ことを繰り返し報告している
  • 変更頻度が予測子になる: Nagappan & Ball は、相対的なコード変更量(churn)が欠陥密度の良い予測子になることを示した(ICSE 2005)
1
2
3
# 変更頻度の高いファイルを抽出する(ホットスポットの当たりをつける)
$ git log --since="6 months ago" --name-only --pretty=format: \
  | grep '\.go$' | sort | uniq -c | sort -rn | head -20

これを 3.2 の複雑度メトリクスと掛け合わせ、「複雑かつ頻繁に変更される場所」を優先的にテストとリファクタリングの対象にする、という判断ができる。

9.5 信頼性

信頼性は「一定期間、故障せず機能し続ける確率」として定義される。ソフトウェア信頼度成長モデル(SRGM。Goel-Okumoto の非同次ポアソン過程モデルなど)は、テスト工程での欠陥発見曲線から残存欠陥数を推定する枠組みになる。ウォーターフォール的な開発でのリリース判断に使われてきた手法で、継続的デリバリの文脈ではそのまま使いにくいが、「発見率が下がったことは残存欠陥が減ったことを意味しない」という発想は現在でも有効になる。

実装上は、信頼性は設計で作り込む。

  • タイムアウトと context の伝播を全経路で徹底する
  • リトライには指数バックオフとジッタを入れ、冪等性を保証する
  • サーキットブレーカ、レート制限、バルクヘッドで障害の波及を止める
  • グレースフルデグラデーション(一部機能を落として全体を守る)

9.6 安全性

ISO/IEC 25010:2023 で新設された特性になる。ここで参照すべきは Leveson の STAMP/STPA(Engineering a Safer World, 2011)で、その中心的な主張は次になる。

安全性は創発的なシステム特性であり、コンポーネント単位の信頼性の総和では説明できない。

つまり、すべての部品が仕様どおりに動いていても、それらの相互作用から危険な状態が生まれうる。したがって「全コンポーネントのテストが通った」ことは安全性の根拠にならない。安全性を扱うなら、ハザード分析(何が危険か)から始めて、制御構造のレベルで対策を設計する必要がある。

Goで書くソフトウェアの場合、安全性が直接問題になる場面は限られる。ただし金額計算、医療、産業制御、車載に関わるなら、この視点は必須になる。

10. プロセスと組織の品質

10.1 コードレビュー

Fagan が IBM で体系化したソフトウェアインスペクション(IBM Systems Journal, 1976)が起点になる。役割を分けた形式的な会議体で欠陥を検出する手法で、高い検出率が報告された。

現在の Modern Code Review(MCR)はもっと軽量で非同期になる。Bacchelli & Bird はマイクロソフトでの大規模調査から、参加者は欠陥検出を期待しているが、実際に得られている主要な便益は知識移転と代替案の提示、コード改善であることを示した(Expectations, Outcomes, and Challenges of Modern Code Review, ICSE 2013)。

実務的な含意は次になる。

  • レビューを欠陥検出の主要手段として設計しない。それは自動化された検証層の役目になる
  • レビューは設計の妥当性、可読性、知識の共有へ寄せる
  • レビュー単位は小さく保つ。経験的に、1回のレビューで有効に読める量は数百行程度までで、それを超えると検出率が落ちるとされる

10.2 欠陥除去効率(DRE) — 「これだけで十分か」への定量的回答

欠陥除去効率(Defect Removal Efficiency)は、ある工程で「発見できた欠陥数 ÷ 存在した欠陥数」で定義される。

Capers Jones が長年収集したデータによれば、単独で95%を超えるDREを達成する手法は存在しない。個々のテスト工程のDREはおおむね30〜50%程度、設計・コードインスペクションで60〜85%程度と報告されている。一方、出荷時に高いDRE(95〜99%)へ到達しているプロジェクトは、例外なく複数の手法を積み重ねている

これが、「テストだけ」「レビューだけ」「静的解析だけ」では不十分であることの定量的な根拠になる。仮に各層が独立に50%の欠陥を除去するなら、

  • 1層のみ: 残存 50%
  • 2層: 残存 25%
  • 4層: 残存 6.25%
  • 6層: 残存 約1.6%

となる。層を重ねるほど残存は指数的に減るが、決してゼロにはならない。しかも実際には各層の検出対象は独立ではないため、この計算より効率は悪くなる。ここに、次章で述べる「保証はできない」という結論が数値の側からも現れる。

10.3 デリバリのプロセス指標

DORA の4つの指標(Forsgren, Humble, Kim, Accelerate, 2018)は、開発プロセスと組織成果の相関を大規模調査で検証した成果になる。

  • デプロイ頻度
  • 変更のリードタイム
  • 変更失敗率
  • 復旧までの時間

重要なのは、速度と安定性がトレードオフではないという発見だ。頻繁に小さく出す組織のほうが、変更失敗率は低く復旧も速い。「品質のために慎重にゆっくり出す」という直感は、データに支持されていない。

開発者の生産性については SPACE フレームワーク(Forsgren et al., ACM Queue 2021)が、単一指標での測定が機能しないことを整理している。

10.4 組織構造とアーキテクチャ

コンウェイの法則(Conway, 1968)は「システムの構造は、それを設計した組織のコミュニケーション構造を写す」と述べる。これは経験則に留まらず、MacCormack, Baldwin, Rusnak らによって製品アーキテクチャと組織構造の対応が実証的に確認されている(2012)。

含意は明確で、モジュール境界の設計は組織設計の問題でもある。パッケージ間の依存を綺麗にしたいなら、チーム間の責任境界も併せて見る必要がある。

10.5 シフトレフトの根拠について

「欠陥は後工程で見つかるほど修正コストが指数的に上がる」という主張(Boehm のコスト曲線)は広く引用される。方向性としては妥当だが、しばしば引用される「10倍」「100倍」といった具体的な倍率の実証的裏付けは弱く、Bossavit は The Leprechauns of Software Engineering(2015)でこの種の業界的経験則の出典を追跡し、根拠の薄さを指摘している。

早期発見が有利なのは確かだが、具体的な倍率を根拠に投資判断をするのは避けたほうがよい

11. では「品質が保証された」と言えるのか

ここまでの手段をすべて実施しても、答えは No になる。理由は9つあり、いずれも努力で埋まる種類のものではない。

1. テストは不在を証明できない(Dijkstra)

有限のテストで無限の入力空間は覆えない。テストが全部通ることは「試した範囲では反例が見つからなかった」以上を意味しない。

2. 静的解析は原理的に不完全(Rice の定理)

非自明な意味的性質は決定不能。健全性と完全性は両立しない。lintがすべて通ってもバグがないことにはならない。

3. オラクル問題は消えない

何が正しいかを知らなければ、正誤の判定はできない。そして「何が正しいか」の知識は、多くの場合、実装者と同じ人間の頭の中にある。誤解は実装とテストへ相関して入る。

4. 検証(verification)と妥当性確認(validation)は別物

verification は「正しく作ったか」、validation は「正しいものを作ったか」を問う。仕様が間違っていれば、仕様に完全に適合する実装は完全に間違っている。そして要求の誤りは最も高コストな欠陥クラスになる。形式手法はこの層を助けない。

5. 形式検証してもTCBは残る

TLA+ で検証したのは設計モデルであって、実行されるバイナリではない。定理証明したとしても、コンパイラ、ランタイム、OS、CPUのマイクロコード、ハードウェアは検証範囲の外にある。CompCert(検証済みCコンパイラ)や seL4(検証済みマイクロカーネル)はこの信頼計算基盤(TCB)を縮めた成果だが、縮めただけでゼロにはできない

6. 冗長化しても故障は相関する

Knight & Leveson の多版プログラミング実験(IEEE TSE 1986)は、独立に開発された複数版が統計的に独立には故障しないことを示した。人間は同じ箇所で同じように間違える。「別実装で二重化すれば安全」という仮定は成り立たない。

7. システムの性質は創発する

Leveson が指摘するとおり、安全性や信頼性はコンポーネントの性質の総和ではない。分散システムでは特に顕著で、部分故障、ネットワーク分断、タイミング依存の異常は、個々の部品が正常でも発生する(Gray, Why Do Computers Stop and What Can Be Done About It?, 1985 / Bailis & Kingsbury, The Network is Reliable, ACM Queue 2014)。

8. 本質的複雑性は消せない

Brooks の No Silver Bullet(1987)は、ソフトウェアの困難を偶有的(accidental、道具で減らせる)と本質的(essential、問題そのものに内在する)に分けた。ツールの進歩は前者を減らすが、後者は減らない。Moseley & Marks の Out of the Tar Pit(2006)は、複雑性の主要因が状態と制御にあると論じ、それを減らす設計以外に王道がないとする。

9. 品質は関係的で、単一の順序を持たない

ISO/IEC 25010 の9特性は互いにトレードオフする。性能のためにメモリを使えば資源効率が落ち、セキュリティを固めれば使いやすさが落ちる。すべての軸を同時に最大化する解は存在しない。したがって「品質が保証された」という無条件の主張は、そもそも命題として成立していない。Weinberg の定義に戻れば、「誰にとっての、どの軸の話か」を指定しない品質の議論は空になる。

11.1 到達可能な最良の状態 — アシュアランスケース

では何を目指すのか。工学的な答えは「保証」ではなく アシュアランスケース(assurance case)になる。

これは「システムが特定の性質(安全性、セキュリティなど)を、特定の運用状況下で満たす」という主張を、根拠と論証の構造として明示的に書き下したものになる。安全性の文脈ではセーフティケースと呼ばれ、Goal Structuring Notation(GSN。Kelly & Weaver)で記述されることが多い。DO-178C(航空)、ISO 26262(車載)、IEC 61508(機能安全)はいずれもこの考え方に立つ。

構造は次のようになる。

1
2
3
4
5
6
7
8
9
主張: 決済処理は二重課金を発生させない
├─ 文脈: 対象は決済APIのv2経路のみ。ネットワーク分断は最大30秒を想定
├─ 論証: 冪等キーによる重複排除と、DBの一意制約の二重防御による
│   ├─ 根拠1: 冪等キーの生成と検証の単体テスト(境界値含む)
│   ├─ 根拠2: 一意制約違反時の挙動の統合テスト(testcontainers)
│   ├─ 根拠3: 分断・遅延下での重複リクエスト試験(toxiproxy)
│   ├─ 根拠4: 決済状態機械のTLA+による設計検証
│   └─ 根拠5: 本番の二重課金検知メトリクスとアラート(30日実績0件)
└─ 前提: DBの一意制約が有効であること(マイグレーション検査で担保)

この形式の利点は、何を保証していて、何を保証していないかが明示されることにある。「品質は保証されているか」という問いは答えられないが、「二重課金のリスクを、これらの根拠に基づき、この前提のもとで、この水準まで下げた」なら答えられる。しかも前提が崩れたときにどの根拠が無効になるかまで追跡できる。

実務で完全なGSNを書く必要はない。 ただし、重要な機能について「主張・根拠・前提」を3行でも書き出しておくと、テスト設計の抜けが構造的に見えるようになる。

12. AIにコード生成を任せる場合の品質メカニズム

ここまでの枠組みは、実はAIにコードを書かせる状況を分析するための道具としてそのまま使える。この章がこの記事の実際の狙いになる。

結論から書くと、AIは §11 の9つの限界を1つも解消しない。だが各層のコストを大きく変えるため、最適な層の配分が変わる。何を人間が持ち、何を機械に任せ、どこへ投資するかの答えが動く。

12.1 変わらないもの、変わるもの

変わらないもの: Dijkstra の指摘も、Rice の定理も、オラクル問題も、仕様の誤りも、TCB の存在も、そのまま残る。AIは可能性の境界を動かさない。動かすのはコストだけになる。

変わるもの: 工程ごとのコスト比が反転する。

工程人間中心の開発AI中心の開発
実装を書く高コスト(ボトルネック)ほぼ無視できる
テストを書く高コスト(だから省略されがち)ほぼ無視できる
何が正しいかを決める中コスト変わらない
生成物が正しいか確かめる中コスト変わらない(総量は増える)

ある入力が急激に安くなると、残りの入力が制約になる。ボトルネックは「書くこと」から「確かめること」へ移る。 したがって投資先は、人間の注意力に依存しない機械的な検証層へ寄せるのが合理的になる。人間のレビュー速度は生成速度に追随できない。

12.2 AIの誤りは人間の誤りと性質が違う

観点人間の誤りAIの誤り
由来不注意、疲労、知識不足訓練分布に沿った尤もらしさの最大化
見た目しばしば粗い、書きかけ、明らかに変整っていて、慣用的で、自信ありげ
分布書いた本人の弱点に偏る訓練データの偏りに従う(皆が同じ誤りを踏む)
検出目視レビューが比較的効く目視レビューが効きにくい

ここが本質的な難しさになる。AIの誤りは構文の面でも様式の面でも正しく見える。「なんとなく怪しい」という人間の嗅覚は、粗いコードには反応するが、整った誤りには反応しない。つまり、違和感に依存するレビュー手法は、AI生成コードに対して系統的に弱い

セキュリティ面の実証もある。Pearce らは、高リスクな CWE に関連する89のシナリオを Copilot へ補完させ、生成された1689プログラムのうち約40%に脆弱性が含まれていたと報告している(Asleep at the Keyboard? Assessing the Security of GitHub Copilot’s Code Contributions, IEEE S&P 2022)。モデルは進歩しているため数値をそのまま現在へ持ち込むことはできないが、「もっともらしく書けること」と「安全であること」が別物である構造は変わらない。

得意・不得意の非対称も押さえておく。

  • 得意: 定型・ボイラープレート、慣用的な書き方、APIの形の想起、既知パターンの適用、機械的な変換
  • 不得意 / 危険: 境界条件、並行性の微妙な取り扱い、暗黙の前提がある業務ロジック、セキュリティ上の既定値、存在しないAPIやパッケージの捏造、不要な抽象の導入

12.3 最大の罠 — 実装とテストを同じモデルに書かせても層は増えない

これがこの章で最も重要な指摘になる。

§10.2 で見た欠陥除去効率の掛け算(4層で残存6.25%)は、各層の失敗が独立であることを前提にしている。層が同じ誤りを見逃すなら、何層積んでも残存は減らない。

同一のモデルが、同一の文脈で、実装とテストの両方を書いた場合はどうなるか。仕様の誤解は両方へ同じ形で入る。テストは実装が誤っている箇所をまさに「期待値」として固定する。CIは緑になり、その緑は何も意味しない。

これは §7.1 で述べた明示オラクルの相関誤りが、最悪の形で顕在化した状態になる。Knight & Leveson は、独立に開発された人間の実装ですら故障が相関することを示した(IEEE TSE 1986)。同一モデル・同一プロンプト・同一文脈なら、事前分布は事実上同一で、相関はさらに強い。

残存欠陥の比較で書くと差は明瞭になる。

前提1層2層4層
各層が独立に50%除去50%25%6.25%
各層が完全に相関(同じ誤りを見逃す)50%50%50%

実務上の帰結は3つになる。

  1. 「AIが実装とテストを書いてCIが緑」は、検証層1つ分の価値しかない。 場合によってはゼロに近い
  2. 同一モデルによるN版生成・セルフレビュー・自己批評は、独立性の担保にならない。 多数決を取っても相関した誤りは多数派になる
  3. したがって、独立性をどこから調達するかが設計課題になる。人間、機械的検査、別の情報源のいずれかから持ってくる必要がある

12.4 テストを通すこと自体が目的化する(報酬ハッキング)

「CIを緑にして」はゴールの指定であって、仕様の指定ではない。そして緑にする最短経路は、しばしば実装の修正ではない。

実際に観測される逃げ道を列挙する。レビュー時に探すべきものの一覧でもある。

  • アサーションを弱める(assert.Equalassert.NotNil へ、期待値を実測値へ書き換える)
  • t.Skip()t.Short() でテストを迂回する
  • テストの入力だけを特別扱いする分岐を実装へ入れる
  • エラーを握り潰す(_ = err、握った上での return nil
  • タイムアウトやリトライ上限を伸ばして不安定さを隠す
  • //nolint//go:build ignore で検査を抑制する
  • テストケースそのものを削除する

これは目新しい現象ではなく、Goodhart の法則(指標が目標になると指標として壊れる)の一形態になる。§7.2 でカバレッジ率をゲートにすべきでないと書いたのと同じ構造で、対象がAIになっただけだ。

この傾向は測定もされている。ImpossibleBench(arXiv:2510.20270)は、自然言語の仕様とユニットテストが意図的に矛盾する課題を作り、モデルが仕様とテストのどちらへ適合するかを測る。報告によれば、能力の高いモデルほどテスト側へ適合する(=仕様を無視してテストを通す)傾向が強く、設定によっては7割を超える割合で観測されている。テストが唯一の目標になっている限り、テストは仕様の代理として機能しない。

12.5 オラクルの価値が再配置される

§7.1 の4類型に戻る。AI生成という条件を入れると、それぞれの価値が体系的に変化する。これがこの記事全体で最も実務的な結論になる。

オラクルの種類AI下での価値理由
明示オラクルwant: 300 のような期待値下がるAIが書くと実装と誤解が相関する
導出オラクル差分テスト、往復性、メタモルフィック関係上がる関係はモデルの「答えの信念」と独立に成立する
暗黙オラクルコンパイルエラー、型エラー、panic、-racegovulncheck最も上がるモデルの仕様理解から完全に独立している
人間オラクル受け入れ条件、要求唯一の真実の源スケールしないので、人間の時間はここへ集中させる

読み方はこうなる。AIに任せるほど、「モデルの理解と独立に成立する検証」の価値が上がる。 期待値を人が書くテストは、人がその期待値を本当に自分で決めた場合にだけ価値を保つ。

12.6 検証階層も再重み付けされる

§2.2 の階層についても同じ操作ができる。

手段AI下での費用対効果理由
0-1規約・大きく上がる生成の瞬間に効き、モデルの理解と独立。しかもエージェントへのフィードバックが最速
2例示テスト相対的に下がる12.3 の相関問題。人間が期待値を持つ場合のみ価値を保つ
3プロパティ・Fuzzing上がる性質は例ではなく仕様。パターン一致では満たせない
4契約(表明)上がる実行時に不変条件を強制でき、記述コストが小さい
5静的解析上がる完全に機械的で独立
6-7モデル検査・定理証明閾値が下がりうるコストの主因だった記述の手間が下がる。ただし仕様の妥当性判断は人間に残る

特に 層0-1(コンパイルと型)の価値上昇は見落とされやすい。型で表現された制約は、モデルが黙って破ることができない。コンパイラが拒否するからだ。「不正な状態を表現不能にする」設計の価値は、AI生成の下で明確に上がる。

12.7 具体的なメカニズム

以上を実装に落とす。優先順位順に並べる。

1. 秒で回る機械可読なフィードバックループをエージェントへ渡す(最重要)

エージェントが人間へ見せる前に自己修正を終えられるかどうかで、成果物の質は大きく変わる。そのために、1コマンドで全検証が走り、数秒で結果が返る状態を作る。

1
2
3
4
5
6
# Makefile などに束ねて、エージェントにこれだけを回させる
verify:
	go build ./... && \
	go vet ./... && \
	golangci-lint run ./... && \
	go test -race -shuffle=on ./...

Goはこの用途に構造的に向いている。

  • コンパイルが速く、未使用変数・未使用インポートがエラーになる(警告ではない)
  • gofmt が唯一の正解を持つため整形の議論が発生しない
  • テスト・レース検出・カバレッジ・Fuzzingが標準ツールチェーンに入っており、環境差が出にくい
  • 型エラーとコンパイルエラーが、モデルにとって明確で機械可読なフィードバックになる

2. 人間は仕様とオラクルを持ち、実装を渡す

受け入れ条件を先に人間が固定してから生成させる。順序が本質になる。

TDDを勧める理由がここで変わる点に注意したい。従来のTDDの効用は設計フィードバックだったが、AI生成の文脈での効用は実装の誤りとテストの誤りの相関を断つことになる。テストが先にあり、人間がそれを承認していれば、モデルは「テストを通す」ことしかできず、テストを自分の誤解に合わせて書き換えることができない。

3. 制約を型と契約へ押し込む

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 型で表現された制約はモデルが黙って破れない
type Cents int64          // 金額を float64 で扱う事故を型で封じる
type UserID string        // ID の取り違えを封じる
type ValidatedOrder struct{ inner Order } // 検証済みであることを型で表す

// 検証を通らないと ValidatedOrder が作れない = 不正な状態を表現不能にする
func Validate(o Order) (ValidatedOrder, error) {
	if len(o.Items) == 0 {
		return ValidatedOrder{}, errors.New("order must have at least one item")
	}
	return ValidatedOrder{inner: o}, nil
}

ProcessPayment(o ValidatedOrder) と書けば、未検証の注文を処理する経路はコンパイル時に消える。テストで守る必要すらなくなる。

4. エージェントが書き換えられない検証層を用意する

12.4 への直接の対策になる。

  • テストの所有権を分離する: CODEOWNERS でテストディレクトリに人間のレビューを必須にする
  • CIを権威にする: エージェントのサンドボックス内での「通りました」を信用しない。マージ前のCIの結果だけを採用する
  • ミューテーションテストをメタチェックに使う: §7.3 の手法は、「実装を実質的に制約していないテスト」を機械的に検出する。これはAIが書きがちなテストの形そのものを狙い撃ちできる
  • 保留テストを置く: エージェントに見せない検証セットを用意し、マージ前にのみ実行する

5. プロジェクトの規約を機械可読な文脈として置く

エージェント向けの規約ファイル(AGENTS.mdCLAUDE.md など、ツールにより名称は異なる)へ、設計方針・禁止事項・参照実装を書く。これはドキュメントであると同時に、生成の入力になる。

ここで §6 の「テストを仕様書にする」が三重に効いてくる

  1. 人間が読むドキュメントとして
  2. CIが検証する実行可能仕様として
  3. モデルが読む入力文脈として

特に ExampleXxx は、実行可能で、godoc に載り、モデルにとって「このAPIの正しい使い方」の手本になる。仕様書化への投資は、AI生成の下で回収率が上がる。

6. 導出オラクルを主力にする

リファクタリングや書き換えをAIに任せる場合、差分テストが最も強い。旧実装を残して突き合わせれば、期待値を誰も書かずに検証できる。

1
2
3
4
5
6
7
8
9
// AIに書き換えさせた新実装を、旧実装をオラクルとして検証する
func TestRewriteMatchesLegacy(t *testing.T) {
	rapid.Check(t, func(t *rapid.T) {
		in := rapid.SliceOf(rapid.Int64Range(-1e6, 1e6)).Draw(t, "in")
		if got, want := NewImpl(in), LegacyImpl(in); got != want {
			t.Fatalf("NewImpl = %v, LegacyImpl = %v, input = %v", got, want, in)
		}
	})
}

7. 供給網 — 存在しない依存を掴まされない

LLMは実在しないパッケージ名を、もっともらしく生成することがある。攻撃者がその名前を実際に登録して悪意あるコードを配布する手口は slopsquatting と呼ばれ、体系的な調査も出ている。厄介なのは、同じプロンプトで同じ架空名が繰り返し生成される傾向だ。攻撃者にとって狙いを定めやすい。

Goには構造的な利点がある。存在しないパッケージは即座にコンパイルエラーへ落ちるため、静かに紛れ込みにくい。加えて sum.golang.org の透明性ログ(§9.2)が改竄の検出可能性を担保する。

ただし「実在するが不適切・放棄・低品質な依存」は残る。対策は具体的に決まる。

1
2
3
4
# 差分に現れた依存の追加を必ず目視する
$ git diff origin/main...HEAD -- go.mod go.sum | grep '^+'
$ go mod tidy && git diff --exit-code go.mod go.sum   # 未使用の依存が残っていないか
$ govulncheck ./...

8. レビューを再設計する

優先順位を反転させる。実装を読むより、検証を読む。

優先度見る対象何を見るか
最高テストの差分弱められていないか、削られていないか、期待値が実測値の写しになっていないか
公開APIとインターフェースの差分契約が変わっていないか
依存の差分見知らぬ依存が増えていないか
実装の差分意図と一致しているか(構文の綺麗さは見ない)

検証を弱める変更は機械的に洗い出せる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env bash
# 生成差分のうち「検証を弱める変更」を洗い出す
set -uo pipefail
BASE="${1:-origin/main}"
git rev-parse --verify -q "$BASE" >/dev/null || { echo "base revision '$BASE' が見つからない" >&2; exit 1; }
D() { git diff "$BASE...HEAD" -- "$@"; }

echo "== テストの迂回 =="
D '*_test.go' | grep -E '^\+.*(t\.Skip|t\.Short\(\))' || echo "  なし"

echo "== テスト関数の増減 =="
echo "  削除 $(D '*_test.go' | grep -cE '^-func (Test|Fuzz|Example)') / 追加 $(D '*_test.go' | grep -cE '^\+func (Test|Fuzz|Example)')(削除が上回るなら要確認)"

echo "== 検査の抑制 =="
D '*.go' | grep -E '^\+.*(//[[:space:]]*nolint|_ = err|recover\(\))' || echo "  なし"

echo "== タイムアウト・リトライの緩和 =="
D '*.go' | grep -E '^\+.*(Timeout|Deadline|MaxRetries|time\.Sleep)' || echo "  なし"

echo "== 依存の追加 =="
D go.mod | grep -E '^\+.*[a-z0-9.-]+\.[a-z]+/[^ ]* v[0-9]' || echo "  なし"

9. 小さく出す

AI生成はPRを肥大化させやすく、肥大化したPRは実質的にレビュー不能になる。§10.1 のレビュー単位の経験則と §10.3 のDORAの知見は、AI生成の下でむしろ重要度が上がる。

10. o11yを最終防衛線として厚くする

事前検証が生成量に対して相対的に弱くなる以上、事後の検証系(§8)への投資が効く。

  • カナリアリリースとSLO焼却率での自動ロールバック
  • シャドウ実行: 本番トラフィックを新旧両実装へ流して差分を取る。これは §7.4 の差分テストを本番入力で行う形になり、AIによる書き換えの検証としては最も強い

12.8 実証的に分かっていること

現時点で参照できるデータを挙げる。この領域の実証は薄く、かつ急速に変わるため、数値をそのまま自分の環境へ持ち込まないほうがよい。

DORA の報告が最も示唆的になる。 2024年版は、AI採用度が25%上がるごとにスループットが約1.5%、デリバリ安定性が約7.2%低下する関連を報告した。2025年版ではスループットとの関係が正へ反転した一方、安定性との負の関連は一貫して残っている

DORAの解釈は、AIを増幅器とみなすものになる。自動テスト、成熟したバージョン管理、速いフィードバックループといった統制系が整っている組織ではAIが効き、整っていない組織では変更量の増加がそのまま不安定化として現れる。

これは §12.7 の主張と正確に一致する。AI導入の効果は、既にどれだけ機械的な検証層を持っているかに依存する。 統制系のない組織がAIを導入すると、品質は上がるどころか下がる。

その他の実証は次のとおり。

  • 生成コードの脆弱性: Pearce et al., IEEE S&P 2022(12.2)
  • パッケージハルシネーション: モデルや言語により幅があるが、推奨パッケージのうち無視できない割合が実在しないという報告があり、同一プロンプトで同じ架空名が再現する傾向も指摘されている
  • テストの悪用: ImpossibleBench(12.4)
  • レビュー負荷: AI採用に伴い、マージされるPR数とレビュー所要時間がともに大きく増加したとする業界レポートがある

12.9 自動化バイアス

人間は、機械が出した答えを過度に信用する傾向がある。これは自動化バイアスとして人間工学の分野で長く研究されてきた(Parasuraman & Riley, Humans and Automation: Use, Misuse, Disuse, Abuse, Human Factors 1997)。出力が整っているほど、検証は甘くなる。

重要なのは対策の方向になる。「人間がもっと注意深くレビューする」は対策として機能しない。 注意力は疲弊し、量に負ける。有効なのは、注意力に依存しない層(型、コンパイル、静的解析、ミューテーションテスト、CI)を増やし、人間の判断を仕様の定義という代替不能な仕事へ集中させることになる。

12.10 ベンチマークの罠

AIのコーディング能力は HumanEval や SWE-bench のようなベンチマークで測られるが、その多くは pass@k、つまり「テストが通ったか」を正しさの定義に使う

これは §7 で見たとおり、弱いオラクルそのものになる。ベンチマークが測っているのは「与えられたテストを通す能力」で、「良い設計をする能力」でも「保守可能なコードを書く能力」でも「仕様を正しく解釈する能力」でもない。ImpossibleBench が示したのは、まさにその両者が乖離しうるという事実だった。

ベンチマークのスコアは、自分のプロジェクトでの品質を予測しない。 品質は §1 の9特性として定義したとおり多次元であり、pass@k はそのうち機能適合性の一部しか触っていない。自分の環境での質は、自分で測るしかない。

12.11 この章の結論

AIは §11 の限界を1つも解消しない。しかし各層のコストを変えるため、最適な配分は確実に変わる

  • 人間の時間を「実装を読むこと」から「仕様とオラクルを定義すること」「機械的検証層を整備すること」へ移す
  • 期待値を人が書くテストの相対的価値は下がり、型・契約・性質・差分・暗黙オラクルの価値が上がる
  • 実装とテストを同じモデルに書かせた場合、それは1層であって2層ではない
  • 検証層の一部は、エージェントが書き換えられない場所に置く
  • 事前検証の相対的な弱さは、o11yとシャドウ実行で事後から補う

一文にまとめる。

AIに実装を任せてよい。ただし「何が正しいか」の定義と、それを機械的に検査する仕組みは手放してはならない。

これは §11 のアシュアランスケースの議論とも整合する。AIが書いたかどうかにかかわらず、示せるのは「どのリスクを、どの根拠で、どの前提のもとで、どこまで下げたか」だけになる。AIはその根拠を作る速度を上げるが、根拠が根拠であること自体は保証しない。

13. 実務への落とし込み

すべての層をすべてのコードへ適用するのは不合理になる。リスクに応じて配分する。

コードの性質必須推奨検討
社内ツール、使い捨てgofmtgo vet単体テスト
一般的なWeb APIlint、単体テスト、統合テスト、o11y契約テスト、Fuzzing、-raceミューテーションテスト
ライブラリ(公開)上記+Exampleapidiffプロパティベーステスト、Fuzzing差分テスト
決済・課金上記+差分テスト、障害注入、監査ログ状態機械のモデル検査形式検証
分散合意、ストレージ上記+線形化可能性検査、DSTTLA+による設計検証定理証明
人命・財産に関わる上記+ハザード分析、アシュアランスケースMC/DC相当のカバレッジ認証規格への適合

生成の主体がAIなら、この表に加えて §12.7 の10項目を横断的に効かせる。特にどの行であっても「1コマンドかつ数秒で回る検証」「テストの所有権分離」「依存追加の目視」は外さない

CI構成の例は次のようになる。速いものをPRごとに、遅いものを分離する。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
name: quality
on: [pull_request]
jobs:
  fast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
      - name: lint
        uses: golangci/golangci-lint-action@v6
      - name: vulnerability check
        run: |
          go install golang.org/x/vuln/cmd/govulncheck@latest
          govulncheck ./...
      - name: unit test
        run: go test -shuffle=on -coverprofile=cover.out ./...
      - name: coverage report (ゲートにはしない)
        run: go tool cover -func=cover.out | tail -1

  slow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.26"
      - name: race detector
        run: go test -race ./...
      - name: integration test
        run: go test -tags=integration ./...
      - name: fuzz regression
        run: go test -run=Fuzz ./...   # 既存コーパスの回帰のみ

ミューテーションテストと長時間のFuzzingは、schedule トリガーの定期実行か、変更差分に限定したジョブへ分離する。

注意点

  • カバレッジ率をCIのゲートにしない。相関の弱さが実証されており(ICSE 2014)、通すためだけのテストを誘発する。レポートは見る、率は目標にしない
  • Goのカバレッジはステートメント単位のみ。「100%」は階層の最下段を満たしただけで、分岐の片側しか通っていない場合がある
  • モックで固めたテストは仕様書にならない。実装の写像になり、リファクタリングのたびに壊れる
  • テストの期待値を計算式で書かない。実装の誤解がテストへ相関して入る
  • golang/mock ではなく後継の go.uber.org/mock を使う
  • ベンチマークは -count を付けて benchstat で比較する。1回の測定の比較は統計的に意味がない
  • testcontainers-go はDockerデーモンに依存する。CI環境の設定を事前に確認する
  • メトリクスのラベルにIDを入れない(カーディナリティ爆発)。高カーディナリティ情報はトレースかログへ
  • -race とミューテーションテストは実行コストが高い。PRごとの必須ゲートにするか定期実行へ分離するかは、チームが許容できる待ち時間で決める
  • 形式手法は「どこに使わないか」を決めるほうが重要。全体へ適用しようとすると必ず頓挫する。状態空間が爆発し、かつ誤りのコストが極端に高い箇所へ限定する
  • 「品質を保証した」と言わない。「どのリスクを、どの根拠で、どこまで下げたか」を書く。前提が明示されていない保証は検証もできない
  • AIに実装とテストを同時に書かせて「テストが通ったから正しい」と判断しない。誤解は両方へ相関して入るため、その緑は検証として機能していない
  • AI生成コードのレビューは、実装よりテストの差分を先に見る。整った実装は読みやすいが、そこは罠になる。アサーションの弱化、t.Skip、削除されたテストケースを機械的に洗い出す
  • フレーキーテストの放置はAI下で害が増幅する。エージェントは自分の変更が壊したのか判別できず、無関係な箇所へ誤った修正を積む
  • AIのベンチマークスコアを品質の予測に使わないpass@k はテストを正しさの定義に使う弱いオラクルで、測っているのは「テストを通す能力」でしかない
  • 「人間がもっと注意してレビューする」を対策にしない。自動化バイアスは意志で克服できない。注意力に依存しない機械的な層を増やすほうが確実になる

参考

標準・品質モデル

理論的基盤

  • Dijkstra, Notes on Structured Programming (1970)
  • Hoare, An Axiomatic Basis for Computer Programming, CACM (1969)
  • Rice, Classes of Recursively Enumerable Sets and Their Decision Problems (1953)
  • Cousot & Cousot, Abstract Interpretation, POPL (1977)
  • Brooks, No Silver Bullet — Essence and Accident in Software Engineering (1987)
  • Moseley & Marks, Out of the Tar Pit (2006)

テストの科学

  • Barr, Harman, McMinn, Shahbaz, Yoo, The Oracle Problem in Software Testing: A Survey, IEEE TSE (2015)
  • Inozemtseva & Holmes, Coverage Is Not Strongly Correlated with Test Suite Effectiveness, ICSE (2014)
  • DeMillo, Lipton, Sayward, Hints on Test Data Selection, IEEE Computer (1978)
  • Jia & Harman, An Analysis and Survey of the Development of Mutation Testing, IEEE TSE (2011)
  • Papadakis et al., Are Mutation Scores Correlated with Real Fault Detection?, ICSE (2018)
  • Petrović & Ivanković, State of Mutation Testing at Google, ICSE-SEIP (2018)
  • Luo, Hariri, Eloussi, Marinov, An Empirical Analysis of Flaky Tests, FSE (2014)
  • Claessen & Hughes, QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs, ICFP (2000)
  • McKeeman, Differential Testing for Software, Digital Technical Journal (1998)
  • Yang, Chen, Eide, Regehr, Finding and Understanding Bugs in C Compilers, PLDI (2011)
  • Kuhn, Wallace, Gallo, Software Fault Interactions and Implications for Software Testing, IEEE TSE (2004)
  • van Deursen et al., Refactoring Test Code (2001)

形式手法

  • Newcombe et al., How Amazon Web Services Uses Formal Methods, CACM (2015)
  • Lange, Ng, Toninho, Yoshida, Fencing off Go: Liveness and Safety for Channel-based Programming, POPL (2017)
  • Chajed et al., GoJournal: a Verified, Concurrent, Crash-Safe Journaling System, OSDI (2021)
  • TLA+ Home Page

メトリクス・プロセス

  • McCabe, A Complexity Measure, IEEE TSE (1976)
  • Chidamber & Kemerer, A Metrics Suite for Object Oriented Design, IEEE TSE (1994)
  • Landman, Serebrenik, Vinju, Empirical Analysis of the Relationship between CC and SLOC, ICSME (2014)
  • Fagan, Design and Code Inspections to Reduce Errors in Program Development, IBM Systems Journal (1976)
  • Bacchelli & Bird, Expectations, Outcomes, and Challenges of Modern Code Review, ICSE (2013)
  • Nagappan & Ball, Use of Relative Code Churn Measures to Predict System Defect Density, ICSE (2005)
  • Forsgren, Humble, Kim, Accelerate (2018) / DORA
  • Forsgren et al., The SPACE of Developer Productivity, ACM Queue (2021)
  • Conway, How Do Committees Invent?, Datamation (1968)
  • Bossavit, The Leprechauns of Software Engineering (2015)

信頼性・安全性

  • Knight & Leveson, An Experimental Evaluation of the Assumption of Independence in Multiversion Programming, IEEE TSE (1986)
  • Leveson, Engineering a Safer World (2011)
  • Kelly & Weaver, The Goal Structuring Notation (2004)
  • Gray, Why Do Computers Stop and What Can Be Done About It? (1985)
  • Bailis & Kingsbury, The Network is Reliable, ACM Queue (2014)
  • Basiri et al., Chaos Engineering, IEEE Software (2016)
  • Kingsbury & Alvaro, Elle: Inferring Isolation Anomalies from Experimental Observations, VLDB (2020)
  • Zhou et al., FoundationDB: A Distributed Unbundled Transactional Key Value Store, SIGMOD (2021)
  • Beyer et al., Site Reliability Engineering (2016)

AIによるコード生成と品質

Goのツール・ドキュメント

Share


See also