更新履歴(初版のみ・2026年11月01日公開)
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.23852479)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「AIが書いたテストを信用してよいか ── 通るのに何も確かめないテストの見分け方」合同会社小村ソフト. https://doi.org/10.5281/zenodo.23852479 https://comcomponent.com/blog/ai-generated-tests-quality/
- DOI(最新版)
- 10.5281/zenodo.23852479
- DOI(この版)
- 10.5281/zenodo.23852480
AIのコーディング支援を使うと、実装と一緒にテストも書いてもらえます。テストが全部通れば安心したくなりますが、テストが確かめているのは、そのテストのアサーションに書いた内容だけです。アサーションが弱かったり、期待値が実装から写したものだったりすると、誤った実装でもテストは通ります。
この記事はシリーズ「AIが書いたプログラムの品質を上げる」の1本目で、通るのに何も確かめないテストを5つの型に分けて、レビューで見分ける方法を説明します。人が書いたテストにも同じ型は現れますが、AIにテストまで任せると、実装とテストを同じ書き手が同じ前提で作ることになり、見落としが重なることがあります。そこで、型ごとにどこを読めばよいかと、どの道具で補えるかを示します。
例は C# と xUnit で書きますが、考え方はほかの言語とテストフレームワークでも同じです。コードは説明のために書いた例で、特定のプロジェクトのものではありません。例は .NET 8 SDK と xUnit で実行し、どのテストが通りどのテストが失敗するかが本文のとおりになることを確かめています。AIが悪いテストをどのくらいの頻度で書くかといった統計は扱いません。
シリーズの残りの3本はコードカバレッジでテストされていないコードを見つける、ミューテーションテストでテストを確かめる、プロパティベーステストでAIが思いつかない入力を試すです。
テストの合否を決めるのはアサーション
ユニットテストは準備(Arrange)、実行(Act)、確認(Assert)の3つの部分で書くのが一般的です1。合否を決めるのは最後の確認、つまりアサーションです。準備と実行がどれだけ丁寧でも、アサーションが何も確かめていなければ、そのテストは失敗しようがありません。
flowchart TB
accTitle: ユニットテストの3つの部分と合否の決まり方
accDescr: 準備と実行のあとにアサーションで結果を期待値と比べ、合否が決まり、アサーションが何も確かめていなければ常に通ることを示す。
A["準備(Arrange)"] --> B["実行(Act)"]
B --> C["確認(Assert)"]
C --> D{"結果が期待値と一致するか"}
D -->|"一致する"| E["通る"]
D -->|"一致しない"| F["失敗する"]
C -.-> G["何も確かめていなければ常に通る"]
図1: テストの合否はアサーションで決まり、アサーションが確かめていないことはテストも確かめていない。
アサーションは結果を期待値と比べます。この期待値、言い換えると「正しい結果はこれだ」という判断の拠りどころを、ソフトウェアテストの研究ではテストオラクルと呼びます。テストオラクルはテスト対象の正しい振る舞いと誤った振る舞いを見分ける手続きのことです2。テストを読むときは、アサーションが何を比べているかと、比べる相手の期待値がどこから来たかの2点を見ます。
Microsoft の .NET のテストの指針も、よいユニットテストの性質として、人の手を借りずに合否を判定できること(Self-Checking)を挙げています1。合否を自動で判定するのはアサーションなので、アサーションの中身がそのままテストの中身です。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全6件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
通るのに何も確かめないテストの5つの型
ここからは次の小さなクラスを例にします。小計が10,000円以上なら10%引きにする、という決まりを実装したものです。
public sealed class PriceCalculator
{
public decimal Total(decimal subtotal)
{
if (subtotal >= 10000m)
{
return subtotal * 0.9m;
}
return subtotal;
}
}
このクラスに対して、通るのに何も確かめないテストの型を5つ挙げます。
flowchart TB
accTitle: 通るのに何も確かめないテストの5つの型
accDescr: テストのコードに現れるアサーションの弱さ、実装から写した期待値、代役の使いすぎ、正常系だけのテストの4つと、変更の経緯に現れる不具合に合わせた書き換えの1つを示す。
T["通るのに何も確かめないテスト"] --> G1["テストのコードに現れる型"]
T --> G2["変更の経緯に現れる型"]
G1 --> A["アサーションが弱い"]
G1 --> B["期待値が実装の写し"]
G1 --> C["代役の使いすぎ"]
G1 --> D["正常系だけ"]
G2 --> E["不具合に合わせた書き換え"]
図2: 5つの型は確かめ方、期待値の出どころ、置き換えた範囲、試した入力、変更の経緯のどこに弱さがあるかで分かれる。
アサーションがない・弱い
いちばん分かりやすいのは、アサーションがないか、ほとんど何も言っていないテストです。
[Fact]
public void Total_LargeOrder_DoesNotThrow()
{
var calculator = new PriceCalculator();
calculator.Total(12000m);
}
[Fact]
public void Total_LargeOrder_ReturnsPositive()
{
var calculator = new PriceCalculator();
var actual = calculator.Total(12000m);
Assert.True(actual > 0);
}
1つ目は例外が出なければ通ります。2つ目は割引を忘れて12,000円を返しても、割引率を間違えて6,000円を返しても通ります。Martin Fowler は、アサーションのないテストでもコードは実行されるので、カバレッジは100%にできると書いています3。実行されたことと、正しく動いたことは別です。
レビューではテストのメソッド名より先にアサーションの行を読みます。Assert.NotNull、Assert.True(x > 0)、件数だけを比べる Assert.Equal(3, list.Count) のように、誤った結果でも満たされる条件しか書いていなければ、このテストは名前が言っていることを確かめていません。
flowchart TB
accTitle: アサーションが弱いテストが通る範囲
accDescr: 正しい実装、割引を忘れた実装、割引率を誤った実装のどれも、結果が0より大きいという弱いアサーションを満たして通ることを示す。
W["Assert.True(結果が0より大きい)"]
R["正しい実装: 10,800円"] --> W
X["割引を忘れた実装: 12,000円"] --> W
Y["割引率を誤った実装: 6,000円"] --> W
W --> P["どれも通る"]
図3: 弱いアサーションは正しい実装と誤った実装を見分けられない。
期待値を実装から写している
次は、アサーションはあるのに、期待値を実装と同じ計算で作っている型です。
[Fact]
public void Total_LargeOrder_AppliesDiscount()
{
var calculator = new PriceCalculator();
var subtotal = 10000m;
var expected = subtotal >= 10000m ? subtotal * 0.9m : subtotal;
var actual = calculator.Total(subtotal);
Assert.Equal(expected, actual);
}
仕様が本当は「10,000円を超えたら割引」だったとします。実装の >= は誤りですが、テストも同じ >= で期待値を作っているので、両者は一致してテストは通ります。期待値が実装の写しになっていると、テストは実装と同じ誤りを持ちます。
Microsoft の指針はテストの中に if や for、文字列の組み立てなどのロジックを書かないよう勧めています。テストにロジックを入れると、テスト自体に不具合が入る可能性が大きく上がるからです1。期待値は入力に対して仕様から決まる値をそのまま書きます。
public static TheoryData<decimal, decimal> BoundaryCases => new()
{
{ 10000m, 10000m },
{ 10001m, 9000.9m },
};
[Theory]
[MemberData(nameof(BoundaryCases))]
public void Total_DiscountStartsAboveTenThousand(decimal subtotal, decimal expected)
{
var calculator = new PriceCalculator();
var actual = calculator.Total(subtotal);
Assert.Equal(expected, actual);
}
この形なら、仕様の「超えたら」と実装の >= の食い違いで、10,000円の場合が失敗します。C# の属性の引数には decimal の値を書けないので、InlineData の代わりに TheoryData で入力と期待値を渡しています。
flowchart TB
accTitle: 期待値の出どころによる違い
accDescr: 期待値を実装と同じ計算で作るとテストも実装と同じ誤りを持って通り、仕様から期待値を書くと実装の誤りで失敗することを示す。
S["仕様: 10,000円を超えたら割引"]
I["実装: 10,000円以上で割引(誤り)"]
I --> C1["実装と同じ計算で作った期待値"]
C1 --> P1["誤りが一致して通る"]
S --> C2["仕様から書いた期待値"]
C2 --> F1["実装の誤りで失敗する"]
図4: 期待値は仕様から書くと、実装の誤りを見つけられる。
代役の使いすぎ
テストではデータベースや外部のサービスなどの依存を、テスト用の代役に置き換えることがあります。こうした代役をまとめてテストダブルと呼びます。決まった値を返すスタブや、呼ばれ方を記録して確かめるモックなどがあります4。代役を使えば、遅い依存や不安定な依存を切り離して、確かめたい部分だけを試せます1。
一方で、確かめたいはずの処理まで代役に置き換えると、テストは代役が返した値を受け取っているだけになります。
public interface IPriceCalculator
{
decimal Total(decimal subtotal);
}
public sealed class OrderService
{
private readonly IPriceCalculator _calculator;
public OrderService(IPriceCalculator calculator) => _calculator = calculator;
public decimal Checkout(decimal subtotal) => _calculator.Total(subtotal);
}
public sealed class FixedPriceCalculator : IPriceCalculator
{
private readonly decimal _result;
public FixedPriceCalculator(decimal result) => _result = result;
public decimal Total(decimal subtotal) => _result;
}
[Fact]
public void Checkout_LargeOrder_ReturnsDiscountedTotal()
{
var service = new OrderService(new FixedPriceCalculator(10800m));
var actual = service.Checkout(12000m);
Assert.Equal(10800m, actual);
}
テストの名前は「割引後の合計を返す」ですが、割引の計算は一度も実行されていません。10,800円はテストが代役に渡した値で、OrderService はそれをそのまま返しただけです。このテストは OrderService が計算結果を返すことは確かめますが、割引が正しいことは確かめません。割引そのものを確かめる別のテストがないなら、割引の計算はテストされていないことになります。
flowchart TB
accTitle: 代役で置き換えた範囲と確かめている範囲
accDescr: テストが割引の計算を固定値を返す代役に置き換えると、OrderServiceが値を受け渡すことだけを確かめ、割引の計算は実行されないことを示す。
T["テスト"] --> O["OrderService(本物)"]
O --> D["代役: 10,800を返すだけ"]
D -.-> N["割引の計算は実行されない"]
T --> A["10,800が返ることを確かめる"]
図5: 代役に置き換えた処理はそのテストでは確かめていない。
レビューでは何を代役に置き換え、何が本物のまま残っているかを書き出します。テストの名前が言っている処理が代役の側にあれば、そのテストはその処理を確かめていません。代役の使い方については「ユニットテストと結合テストの境界をどう引くか」でも扱っています。
正常系しか試していない
アサーションも期待値も正しくても、試している入力が典型的な値だけ、ということがあります。上の PriceCalculator で12,000円だけを試すテストは、割引の境目の10,000円と10,001円、0円、負の値のどれも試していません。
上の例の >= と > の取り違えは境目の値を試さないと見えません。Microsoft の指針は、よく名前の付いたテストがあれば「空の文字列や null を渡したらどうなるか」にテストが答えてくれる、と書いています1。テストは仕様の説明書を兼ねるので、境目と異常な入力の扱いが書かれていなければ、その仕様は誰も確かめていないことになります。
flowchart TB
accTitle: 正常系だけのテストが通らない分岐
accDescr: 12,000円だけを試すテストは割引する分岐しか通らず、割引しない分岐と境目の値は試されないことを示す。
IN["入力"] --> Q{"小計が10,000円以上か"}
Q -->|"はい"| A["割引する(12,000円で通る)"]
Q -->|"いいえ"| B["割引しない(試していない)"]
Q -.-> E["境目の10,000円と10,001円も試していない"]
図6: 典型的な値だけのテストは片方の分岐と境目を試さない。
試していない分岐はコードカバレッジの分岐の網羅率で見つけられます。たとえば分岐が2つしかないコードで、片方だけを通るテストの分岐カバレッジは50%になります5。
不具合に合わせて書き換えている
最後の型は変更の経緯に現れます。実装を変えたらテストが失敗し、テストの期待値を新しい出力に合わせて書き換えて通した、という流れです。
仕様が変わって期待値も変わったのなら、書き換えは正しい作業です。一方、実装の誤りでテストが失敗したのに期待値を合わせてしまうと、そのテストは不具合を正しい振る舞いとして記録します。AIのコーディングエージェントに「テストが通るまで直して」と頼むと、エージェントにとっては実装を直すのも期待値を直すのも、テストを通す手段です。
sequenceDiagram
accTitle: 不具合に合わせてテストを書き換える流れ
accDescr: 実装の変更でテストが失敗し、期待値を新しい出力に書き換えて通した結果、不具合が正しい振る舞いとして記録される流れを示す。
participant Dev as 実装の変更
participant Test as テスト
Dev->>Test: 実行する
Test-->>Dev: 期待値と違うので失敗
Dev->>Test: 期待値を新しい出力に書き換える
Test-->>Dev: 通る
Note over Test: 誤った出力が正しい振る舞いとして残る
図7: 失敗したテストの期待値を出力に合わせると、不具合がテストに記録される。
いまのコードの振る舞いをそのまま記録するテストには、特性化テストという正当な使い道もあります。テストのない古いコードに手を入れる前に、現在の振る舞いを固定しておくためのものです(「テストのないレガシー業務アプリに安全に手を入れる」)。ただしそれは「正しいかどうかは分からないが、いまはこう動く」と分かったうえで記録するものです。新しく書いたコードの変更で、理由の説明なしに期待値が書き換わっているなら、その書き換えは疑って読みます。
レビューでは実装の差分と一緒にテストの差分を読みます。同じ変更の中でアサーションの期待値が変わっていたら、その値がなぜ変わったのかを、仕様の変更として説明できるかを確かめます。
レビューで見るところ
5つの型はテストのコードを読めば見分けられます。AIが書いたテストをレビューするときは、次の順に読みます。
- アサーションの行を読み、誤った結果でも満たされる条件になっていないかを確かめる。
- 期待値がどこから来たかを確かめる。テストの中の計算で作っていれば、仕様から決まる具体的な値に書き換えてもらう。
- 何を代役に置き換え、何が本物のまま残っているかを書き出し、テストの名前が言っている処理が本物の側にあるかを確かめる。
- 試している入力を書き出し、境目、空、null、異常な値が含まれているかを確かめる。
- 実装の差分と一緒にテストの差分を読み、期待値を書き換えた所があれば、その理由を確かめる。
flowchart TB
accTitle: AIが書いたテストをレビューする順序
accDescr: アサーション、期待値の出どころ、代役で置き換えた範囲、試している入力、テストの差分の順に読み、問題があれば書き直しを頼むことを示す。
A["アサーションの行を読む"] --> B["期待値の出どころを確かめる"]
B --> C["代役と本物の範囲を書き出す"]
C --> D["試している入力を書き出す"]
D --> E["テストの差分を読む"]
E --> F{"問題があるか"}
F -->|"ある"| G["書き直しを頼む"]
F -->|"ない"| H["受け入れる"]
図8: アサーションから順に読み、問題があれば書き直しを頼む。
AIに作業を頼む側でもできることがあります。Claude Code の公式ドキュメントは、「すべてのテストが通った」という報告だけではほとんど何も分からないので、実行したコマンドと出力を証拠として求めるよう勧めています。不具合を直してもらうときは、まず再現する失敗するテストを書かせ、それから直させる頼み方を例に挙げています6。最初に失敗するテストを見ておけば、そのテストが不具合を検出できることを確かめられます。
道具で補う範囲
レビューは人の目に頼るので、テストが多いと見落としが出ます。5つの型のうち4つは道具で機械的に見つける手がかりが得られます。
| 型 | 見つける手がかりになる道具 | シリーズの記事 |
|---|---|---|
| アサーションがない・弱い | ミューテーションテスト | ミューテーションテストでテストを確かめる |
| 期待値を実装から写している | プロパティベーステスト | プロパティベーステストでAIが思いつかない入力を試す |
| 代役の使いすぎ | コードカバレッジ、ミューテーションテスト | コードカバレッジでテストされていないコードを見つける |
| 正常系しか試していない | コードカバレッジ、プロパティベーステスト | コードカバレッジでテストされていないコードを見つける |
| 不具合に合わせて書き換えている | 道具では見つけにくい。テストの差分のレビュー | この記事 |
コードカバレッジはテストがどの行や分岐を実行したかを測ります5。代役に置き換えて一度も実行されない処理や、試していない分岐を見つけるのに向いています。ただし実行されたかどうかしか分からないので、アサーションの弱さは見えません。Microsoft の指針も、カバレッジの高さは成功の指標ではなくコードの品質も意味しない、としています1。Fowler も、カバレッジはテストされていない部分を見つけるには役立つが、テストのよさを示す数字としてはほとんど役に立たないと書いています7。
ミューテーションテストはテスト対象のコードをわざと少し壊し(たとえば >= を > に変える)、テストが失敗するかを確かめます。壊したのにテストが通れば、そのテストはその変化を確かめていません8。.NET では Stryker.NET が使えます9。
flowchart TB
accTitle: コードカバレッジとミューテーションテストが見る範囲
accDescr: コードカバレッジはテストがコードを実行したかを見て、ミューテーションテストは実行したうえで変化を検出できるかを見ることを示す。
CODE["テスト対象のコード"] --> Q1{"テストが実行したか"}
Q1 -->|"いいえ"| U["カバレッジで見つかる"]
Q1 -->|"はい"| Q2{"壊したら失敗するか"}
Q2 -->|"いいえ"| W["ミューテーションテストで見つかる"]
Q2 -->|"はい"| OK["その変化は確かめている"]
図9: カバレッジは実行されていないコードを、ミューテーションテストは実行されているのに確かめていないコードを見つける。
プロパティベーステストは個々の入力と期待値を並べる代わりに、どの入力でも成り立つべき性質(プロパティ)を書き、道具が生成した多数の入力でその性質を確かめます。.NET 向けには FsCheck があり、失敗した入力を小さく絞り込んで反例として示します10。「割引後の金額は小計を超えない」のような性質は実装の計算を写さずに書けるので、期待値を実装から写す型の対策になります。性質は期待値の代わりの判定基準、つまりテストオラクルの一つの形です2。
不具合に合わせた書き換えは、テストのコードだけを見ても、正しい書き換えと区別できません。仕様が変わったかどうかは道具には分からないので、テストの差分を人が読みます。
関連記事
- ユニットテストと結合テストの境界をどう引くか
- テストのないレガシー業務アプリに安全に手を入れる ── 特性化テストとリファクタリングの実践
- コードカバレッジでテストされていないコードを見つける
- ミューテーションテストでテストを確かめる
- プロパティベーステストでAIが思いつかない入力を試す
参考リンク
-
Microsoft Learn, Best practices for writing unit tests. Arrange・Act・Assert、Self-Checking、テストにロジックを書かないこと、テストダブルの用語、コードカバレッジの数値と品質の関係。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Earl T. Barr, Mark Harman, Phil McMinn, Muzammil Shahbaz, Shin Yoo, The Oracle Problem in Software Testing: A Survey. IEEE Transactions on Software Engineering 41(5), 2015。テストオラクルの定義と、仕様などからオラクルを作る方法。 ↩ ↩2
-
Martin Fowler, AssertionFreeTesting. アサーションのないテストでもカバレッジは上がること。 ↩
-
Martin Fowler, TestDouble. テストダブルと、ダミー・フェイク・スタブ・スパイ・モックの区別(Gerard Meszaros の用語)。 ↩
-
Microsoft Learn, Use code coverage for unit testing. コードカバレッジが行・分岐・メソッドの実行を測ることと、分岐カバレッジの例。 ↩ ↩2
-
Claude Code Docs, Best practices for Claude Code. 実行したコマンドと出力を証拠として求めること、再現する失敗するテストを書かせてから直させる頼み方。 ↩
-
Martin Fowler, TestCoverage. カバレッジはテストされていない部分を見つけるのに役立つが、テストのよさを示す数字にはならないこと。 ↩
-
Stryker Mutator, Introduction. ミューテーションテストの仕組みと、カバレッジだけではテストの効果が分からないこと。 ↩
-
Stryker Mutator, Stryker.NET introduction. .NET のプロジェクトでミューテーションテストを行う Stryker.NET。 ↩
-
FsCheck, FsCheck: Random Testing for .NET. プロパティを多数の生成した入力で確かめ、失敗したときに最小の反例を示すこと。C# でも書けること。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
プロパティベーステストで、例では届かない入力を試す ── FsCheckとxUnitによるC#の実践
AIが書いたテストは、書いた側が思いついた入力しか確かめません。どの入力でも成り立つ性質を書き、入力をFsCheckに作らせるプロパティベーステストを、C#とxUnitのコードで説明します。
ミューテーションテストでテストをテストする ── Stryker.NETで確かめていないテストを見つける
コードをわざと少し変えて、テストが失敗するかを見るのがミューテーションテストです。実行しているのに何も確かめていないテストの見つけ方を、Stryker.NETの導入、レポートの読み方、生き残ったミュータントの扱いまで説明します。
カバレッジでテストされていないコードを見つける ── AIが書いたC#のテストを確かめる
AIが書いたC#のテストが、どのコードを実行していないかをカバレッジで調べます。行と分岐の違い、Coverletでの測り方、レポートの読み方、割合を目標にしない理由、生成コードの除外、変更した行だけの確認をまとめます。
業務アプリの日時とタイムゾーン ── DateTimeの罠からUTC保存の原則、テスト設計まで
サーバー移設で時刻が9時間ずれる──日時事故の原因をDateTimeのKindと暗黙変換から整理します。DateTimeOffsetとの使い分け、UTC保存の原則、TimeZoneInfoと夏時間、TimeProviderによるテスト設計まで解説します。
WindowsデスクトップアプリのUI自動テスト ── UI Automationの仕組みとFlaUIで作る壊れにくいテスト
WinForms/WPFアプリのUI自動テストを、Windows UI Automationの仕組みから整理します。FlaUIによる最小実装、AutomationId設計と条件待機で壊れにくくする方法、CI無人実行の罠まで実務目線で解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
AIが書いたコードとテストのどこを人が確かめるか、レビューの観点とテストの方針を整理したい場合に、技術相談・設計レビューとして扱えます。
不具合調査・原因解析
テストは通っているのに本番で不具合が出る場合、どのテストが何を確かめていなかったかを調べるのは、不具合調査・原因解析として進めやすい題材です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- AIが「テストはすべて通りました」と言ったら、そのまま受け入れてよいですか?
- それだけでは判断できません。テストが確かめているのはアサーションに書いた内容だけなので、アサーションが弱い、期待値を実装から写している、代役で中身を置き換えている、といったテストは誤った実装でも通ります。実行したコマンドと出力を受け取り、テストのアサーションと期待値の出どころを読んでから判断します。
- テストの期待値はどこから持ってくればよいですか?
- 仕様や業務のルールから持ってきます。実装と同じ計算をテストの中でもう一度して期待値を作ると、実装の誤りをテストも同じように持ってしまい、両者が一致して通ります。具体的な入力と、仕様から決まる出力をそのまま書くのが基本です。
- コードカバレッジが高ければ、テストは十分ですか?
- 十分とは言えません。カバレッジはテストがどのコードを実行したかを測るもので、結果を確かめたかどうかは測りません。アサーションのないテストでもカバレッジは上がります。実行されていないコードを見つける道具として使い、確かめ方の弱さはミューテーションテストなどで調べます。