atoneグループに所属している山田です。ソフトウェアエンジニアをしています。
2月21日に仙台市で開催されたGo Conference mini Sendai 2026 に参加しました。 Ryo Mimura @r4mimuさん、sivchariさんのセッションを聞き、Mutation Testsを現在開発中のプロダクトに組み込みました。 実行した内容まとめと振り返りを記載します。
お二人の登壇資料はこちら
きっかけ
AIを使ってシステム開発を進める中で、AIが「中身のない(パスするだけの)テストコード」を出力したことがあり、人間が詳細までレビューし続けなければテスト品質を担保できないことに対して、AIを使っての開発をスケールさせていくことに不安を感じていました。
この「スケールへの不安」を解消し、AI活用を推進するため、以下の技術的側面とプロセス的側面の2点に課題を絞り込みました。
- 「中身のないテストコード」をAIが生成するリスクの排除
- 目的:テストの有効性を仕組みで担保すること
- レビュー工程の精度向上と効率化
- 目的:人間による全量レビューというボトルネックを解消し、開発速度を向上すること
今回、Ryo Mimura さん、sivchari さんのセッションを通じて、Rules・レビュー等の工夫による「ソフト制約」だけでなく、物理的に出力を制御する「ハード制約」というアプローチを知りました。Mutation Testsによる物理的ガードレールを導入することで「AIを使っての開発をスケールさせる不安」を根本的に解消できると判断し、導入に踏み切りました。
今回焦点をあてたMutation Testsとは?
ソースコードを意図的に書き換え、テストが失敗するかを確認する手法です。 以下の資料を参考に、自分なりに概要を整理しました。
Introduction to Mutation Testing
テストにおいては「偽陽性」と「偽陰性」の違いが重要です。
- 偽陽性: 実際は異常がないのに、テストが失敗すること。
- 偽陰性: 実際は異常があるのに、テストが成功(見逃し)すること。
偽陽性は「テストが落ちる」ため、気付くことができます。 しかし、偽陰性はテストが通ってしまうため、検出できません。
Mutation Tests は、偽陰性を検出するための手法です。 コードをあえて壊す「Mutant」を仕込み、テストが正しく失敗するかを検証します。
期待通りに失敗すれば「Mutant を撃破(Kill)」できたことになり、テストの信頼性が証明されます。
導入を進めた手順
①導入戦略と運用設計
Mutation Tests は処理が重く、実行時間が伸びる課題があります。 開発生産性を下げないよう、以下の運用ルールを定めました。
- 対象の厳選: 偽陰性のリスクが高い、重要なロジックに絞って導入します。
- 定期実行: 常に動かすのではなく、定期的なバッチ処理として運用します。
また、テスト品質を向上させるという目的を考えると、導入しただけで誰も気にしないようでは改善はされないので、以下を運用に組み込みます。
- 形骸化の防止: 月1回の定例会で結果を議題に上げ、継続的に改善します。
②導入箇所の選定
ビジネスインパクトの大きさを基準に、以下の箇所を優先しました。
- Event を apply するレイヤー
- 現在開発中のマイクロサービスは Event Sourcing (ES) + CQRS を採用しています。
- 履歴から状態を再現する根幹であり、影響範囲が非常に広いためです。
- 金額計算周りのドメインサービス
- 税金計算など、正確性が求められるロジックを対象にします。
③ライブラリの選定
Go の主要ライブラリを比較しました。
go-mutesting:スター数が多い定番ですが、更新頻度は控えめです。
gremlins:マイクロサービスに適しており、精密な検証が可能です。
今回は定期バッチで回すため処理時間の長さは問題にならないこと、AST(Abstract Syntax Tree)による精密な検証が可能なこと、更新が頻繁にされていることを理由に gremlins を採用しました。
※ マイナーバージョンのため、利用の際は破壊的変更にご注意ください。自動テストに載せない運用であれば、大きな問題にはならないと判断しました。

④実装と実行結果
実装は Cursor を活用し、設定ファイルの作成から GitHub Actions への組み込みまで迅速に行いました。 デフォルトでは全ファイルが対象になるため、exclude-files オプションで対象を制限しています。
# Mutation Testing (go-gremlins/gremlins) 設定
# 対象: Event Apply レイヤー5パッケージ + domain/tax(6パッケージのみ)
# それ以外は exclude-files で除外する
unleash:
dry-run: false
output: "mutation-test-results.json"
timeout-coefficient: 3
threshold:
efficacy: 0
mutant-coverage: 0
exclude-files:
// 省略
- "^(cmd|e2e|docs)/"
- "^internal/.../"
- "^internal/command/.../"
初回実行結果

調整後実行結果

初回実行時はタイムアウトが目立ちましたが、設定(timeout-coefficient)を調整した結果、多くの Lived(生き残った Mutant) を検出できました。 出力された Lived の「ファイル:行」を確認し、境界値などを明示的に検証するテストを追加することで、偽陰性のリスクを着実に下げられます。
【実践例】Lived Mutant を検出し、テストで撃破するまで
実際に gremlins が検出した Lived(生き残った Mutant) の具体例を紹介します。
1. 検出された問題:丸め処理の境界値
今回、税金計算ロジック(tax/calculator.go)において、四捨五入の境界値判定が「テスト不足」として指摘されました。
対象コード:
func Calculate(amount money.Money, ratePercent int64) money.Money {
numerator := int64(amount) * ratePercent
denominator := 100 + ratePercent
quotient := numerator / denominator
remainder := numerator % denominator
// 四捨五入: 余りが分母の半分以上なら切り上げ
if remainder*2 >= denominator { // ← ここが Mutant の標的
quotient++
}
return money.Money(quotient)
}
2. 生き残った Mutant (Lived)
gremlins はこのコードに対し、CONDITIONALS_BOUNDARY という変異を仕掛けました。これは実質的に >= を > に書き換える 操作です。
もし、テストコードに「ちょうど半分(0.5)」になるケースが含まれていない場合、>= が > に変わってもテストは成功し続けてしまいます。これが「偽陰性」の状態であり、Mutant が生き残った(Lived)原因でした。
3. テストの追加と Mutant の撃破 (Killed)
この指摘を受け、ちょうど計算結果が 0.5 になるケース(税込3円・税率20%)をテストに追加しました。
追加したテスト:
t.Run("ちょうど半分の境界値_税込3円_税率20%", func(t *testing.T) {
// 3 * 20 / 120 = 60 / 120 = 0.5
// quotient=0, remainder=60, remainder*2=120 == denominator
// なので本来は切り上げて「1円」になるべき
grossAmount := money.Money(3)
tax := Calculate(grossAmount, 20)
assert.Equal(t, money.Money(1), tax, "ちょうど半分は切り上げること")
})
4. 修正後の結果
このテストを追加した状態で make mutation-test を再実行したところ、該当の Mutant は無事に KILLED(撃破) されました。
これにより、「四捨五入の境界値ロジックが、意図せず書き換えられても検知できる」という テストの信頼性 が証明されたことになります。
導入してみて
Mutation Testingを導入したことで、当初の2つの課題を以下のように解消することができました。
「中身のないテストコード」をAIが生成するリスクの排除
Mutation TestをAIによる実装の完了後やGitHub Actionsで定期実行することで、”テストされていないテスト”(偽陰性)を仕組みで防ぐことができるようになりました。
実践例で示した通り、Lived(生き残ったMutant)を検出・撃破するプロセスを通じて、特に重要なロジック(金額計算周りなど)のテストの信頼性を客観的に証明できました。
レビュー工程の精度向上と効率化
破壊的変更に耐えられるテストが存在することを仕組みで確認できるようになったため、人間による詳細なテストコードレビューの負荷を大幅に軽減することができました。
レビューの焦点が、コードの網羅性ではなく、検出されたLived MutantをKilledするための「境界値」や「意図的な変更」の有無に絞られ、精度の高いレビューが可能になりました。
また、副次的効果として、テストの品質を考える機会を創出できたこともとても良かったと思っています。Mutation Testを導入する過程でメンバー同士でテスト品質について議論し、考えることができました。今後も"品質の高いテストとは何か?"という問いをチーム内で議論していきたいです。
最後に
現状、AIは責任を取ることができません。AIはあくまでツールであり、成果物の責任は人間(自分)にあります。だからこそ、指示(ソフト制約)だけでなく、仕組みによるガードレールを作ることが、今後AIと共に開発していく中で重要になると思っています。
今後は Fuzzing Test の導入や、Linter による依存方向の制御など、ハード制約をより整えていきたいです。