更新履歴(初版のみ・2026年11月01日公開)
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.23852481)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「カバレッジでテストされていないコードを見つける ── AIが書いたC#のテストを確かめる」合同会社小村ソフト. https://doi.org/10.5281/zenodo.23852481 https://comcomponent.com/blog/code-coverage-find-untested-code/
- DOI(最新版)
- 10.5281/zenodo.23852481
- DOI(この版)
- 10.5281/zenodo.23852482
AIにコードとテストをまとめて書かせると、テストはすべて通るのに、どのコードを確かめたのかが分からないことがあります。テストの数や「全部成功」という表示だけでは、エラーの処理や境界の条件が一度も実行されていないことに気づけません。
この記事ではそうした「テストが実行していないコード」をコードカバレッジで見つける方法を説明します。カバレッジは、テストを実行したときにどのコードが実行されたかを記録します。実行されていない行や、一度も通っていない分岐が分かれば、そこにテストが要るか、そのコードが要るかを判断できます。
一方で、カバレッジは「実行されたか」しか数えません。テストが結果を正しく確かめているかは分からないので、それはシリーズの次の記事でミューテーションテストを使って調べます。
対象は C# と .NET の単体テスト(xUnit、MSTest、NUnit)です。コマンドとパッケージ名は、2026年11月時点の Microsoft と Coverlet の資料で確かめたものです。例のメソッドとテストは .NET 8 SDK と Coverlet(coverlet.collector)で実際に測り、本文に書いた行と分岐の結果になることを確かめています。
この記事は「AIが書いたプログラムの品質を上げる」シリーズの2本目です。1本目の「AIが書いたテストを信用してよいか」では、通るのに何も確かめないテストの型を扱っています。
コードカバレッジが数えるもの
コードカバレッジは、単体テストが実行したコードの量を、行・分岐・メソッドの単位で表したものです1。たとえば分岐が2つあるコードで、片方の分岐だけを確かめるテストを書くと、分岐のカバレッジは50%になります1。
flowchart TB
accTitle: カバレッジの計測の流れ
accDescr: テストを実行すると計測ツールが実行されたコードを記録し、ファイルに書き出し、レポートで実行された行と実行されていない行を見分ける流れを示す。
T["テストを実行する"] --> C["計測ツールが実行されたコードを記録する"]
C --> F["結果をファイルに書き出す"]
F --> R["レポートで実行の有無を見る"]
R --> Y["実行された行・分岐"]
R --> N["実行されていない行・分岐"]
図1: テストを実行すると、どの行と分岐が実行されたかが記録され、レポートで見分けられる。
行カバレッジと分岐カバレッジ
行カバレッジはその行が1回でも実行されたかを数えます。分岐カバレッジは if 文や条件演算子の分かれ道のそれぞれを通ったかを数えます。この2つは1行の中に条件が書かれているときに食い違います。
次のメソッドを例にします。説明のために作った例です。
public static class PriceCalculator
{
public static decimal Subtotal(decimal unitPrice, int quantity)
{
if (unitPrice < 0) throw new ArgumentOutOfRangeException(nameof(unitPrice));
return quantity > 0 ? unitPrice * quantity : 0m;
}
}
このメソッドに次のテストが1つだけあるとします。
[Fact]
public void Subtotal_MultipliesPriceByQuantity()
{
Assert.Equal(300m, PriceCalculator.Subtotal(100m, 3));
}
このテストではメソッドのどの行も実行されます。if の行では条件が評価され、return の行も実行されるので、行カバレッジでは未実行の行がありません。ところが、例外を投げる道と、数量が0以下のときに0を返す道は、一度も通っていません。分岐カバレッジではこの2つが通っていない分岐として残ります。
flowchart TB
accTitle: 1行に書いた条件と2つのカバレッジ
accDescr: 単価と数量が正のテストだけを実行すると、if の行と return の行はどちらも実行済みになるが、例外を投げる道と0を返す道は通っていないことを示す。
S["テスト: 単価100、数量3"] --> L1["if の行(実行済み)"]
L1 --> B1["例外を投げる道(未通過)"]
L1 --> L2["return の行(実行済み)"]
L2 --> B2["単価×数量を返す道(通過)"]
L2 --> B3["0を返す道(未通過)"]
図2: 行はすべて実行されていても、1行に書いた条件の片方の道は通っていないことがある。
分岐をいくつと数えるかは、計測ツールがコンパイル後のコードから決めます。そのため、レポートに出る分岐の数は、ソースを見て数えた数と一致しないことがあります。数そのものより、どの行に通っていない分岐があるかを見ます。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全5件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
.NETでカバレッジを測る
.NET のカバレッジの道具はテストの実行中に記録を取る「データコレクター」と、記録から読みやすいレポートを作る「レポートジェネレーター」の2種類に分かれます1。記録を取る道具として、.NET に組み込みの Microsoft のコードカバレッジと、オープンソースの Coverlet があります。
どれを使うかは、テストプロジェクトがどのテスト実行基盤で動いているかで決まります。従来の VSTest で動いているか、Microsoft.Testing.Platform(MTP)で動いているかです。
flowchart TB
accTitle: テスト実行基盤ごとのカバレッジの取り方
accDescr: テストプロジェクトがVSTestで動くならcoverlet.collectorを使い、Microsoft.Testing.Platformで動くならcoverlet.MTPかMicrosoft.Testing.Extensions.CodeCoverageを使い、どちらもCobertura形式で出力してReportGeneratorでレポートにすることを示す。
P["テストの実行基盤"] -->|"VSTest"| V["coverlet.collector"]
P -->|"MTP"| M1["coverlet.MTP"]
P -->|"MTP"| M2["Microsoftのコードカバレッジ"]
V --> X["Cobertura形式のXML"]
M1 --> X
M2 --> X
X --> R["ReportGeneratorでHTMLにする"]
図3: テストの実行基盤によって使うパッケージが変わるが、Cobertura形式で出せば同じ道具でレポートにできる。
VSTestで動くテストプロジェクト
dotnet new xunit で作った xUnit のテストプロジェクトには、Coverlet のデータコレクターである coverlet.collector パッケージが最初から入っています1。テストプロジェクトのフォルダーで次のコマンドを実行します。
dotnet test --collect:"XPlat Code Coverage"
"XPlat Code Coverage" は Coverlet のデータコレクターを指す名前です1。実行が終わると、TestResults フォルダーの下に作られる GUID の名前のフォルダーに、coverage.cobertura.xml が出力されます12。
"XPlat Code Coverage" の代わりに "Code Coverage" と書くと、.NET 組み込みのデータコレクターを使います。こちらの出力はバイナリの .coverage ファイルなので、Visual Studio の外で読むには dotnet-coverage ツールなどで読める形式に変換します1。
Microsoft.Testing.Platformで動くテストプロジェクト
coverlet.collector は Microsoft.Testing.Platform(MTP v2)と互換性がありません2。MTP で動かすテストプロジェクトでは、次のどちらかを使います3。
coverlet.MTPパッケージを入れ、dotnet test --coverletで実行します。--coverlet-output-format coberturaで Cobertura 形式を指定できます。Microsoft.Testing.Extensions.CodeCoverageパッケージを入れ、--coverageで実行します。--coverage-output-format coberturaで Cobertura 形式を指定できます。
Microsoft.Testing.Extensions.CodeCoverage は、既定でテストプロジェクト自身のアセンブリを計測の対象から外します3。
ReportGeneratorでHTMLのレポートにする
Cobertura 形式の XML は ReportGenerator で HTML のレポートにできます1。ReportGenerator を .NET のグローバルツールとして入れ、出力された XML を指定して実行します。
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator -reports:"TestResults\{guid}\coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
{guid} の部分は実行ごとに作られるフォルダーの名前に置き換えます。coveragereport フォルダーに作られた HTML を開くと、ファイルごと・行ごとに実行の有無を確かめられます。
レポートからテストされていない分岐を探す
Cobertura形式のXMLで見る項目
Cobertura 形式の XML では、行ごとに hits(実行された回数)が記録されます。分岐のある行には branch="True" と、通った分岐の割合を示す condition-coverage が付きます1。Microsoft の資料の例では、すべての分岐を通った行が condition-coverage="100% (2/2)" と記録されています。
レポートでは次の2つを探します。
hitsが0の行。テストが一度も実行していない行です。condition-coverageが100%に届いていない行。実行はされたが、通っていない分岐が残っている行です。
先に見るところ
実行されていない行がたくさん見つかったときは、全部を同じ重さで扱わず、間違えたときに困る処理から見ます。
- 引数や入力の値を確かめて、不正な値をはじく処理
- 例外を受け止める
catchと、エラーを呼び出し元に返す処理 - null や空のコレクションを扱う分岐
- 再試行やタイムアウトの処理
- 境界の値(0、上限、空文字列)で分かれる条件
AIにテストを書かせたときは、指示した正常な入力の例だけで終わっていないかを、この一覧で確かめます。前の節の例でも、正常な入力のテストだけでは、例外を投げる道と0を返す道が残りました。
flowchart TB
accTitle: レポートで見つけた行の扱い方
accDescr: 実行されていない行と通っていない分岐を見つけたら、そのコードが要るかを判断し、要らなければ削り、要ればテストを足し、実行済みの行も結果を確かめているかを次に調べる流れを示す。
A["レポートの行"] --> B{"実行されているか"}
B -->|"hits が0"| C{"そのコードは要るか"}
B -->|"分岐の一部だけ"| D["残りの分岐のテストを足す"]
B -->|"すべて通過"| E["確かめ方を調べる(次の記事)"]
C -->|"要る"| F["テストを足す"]
C -->|"要らない"| G["コードを削る"]
図4: 実行されていないコードは、テストを足すか、要らないなら削るかを決める。
Google のテストブログの記事は、カバレッジでは実行された部分よりも実行されていない部分のほうが意味を持つとし、カバーされていない具体的な行についてコードレビューで話し合うほうが、恣意的な目標の数字にこだわるより役に立つと書いています4。
割合を目標にしない理由
カバレッジの割合を「80%以上」のように目標にすると、割合を上げること自体が目的になりやすくなります。Martin Fowler は、カバレッジはテストされていない部分を見つける道具としては役に立つが、テストの良さを表す数字としてはほとんど役に立たないと書いています。低い品質のテストでも高い割合には簡単に届くからです5。その極端な例として、コードを実行するだけで結果を確かめない「アサーションのないテスト」を挙げています5。
前の節の Subtotal で確かめてみます。次のテストにはアサーションが1つもありません。
[Fact]
public void Subtotal_DoesNotThrow()
{
PriceCalculator.Subtotal(100m, 3);
PriceCalculator.Subtotal(100m, 0);
}
このテストは条件演算子の両方の道を実行するので、Assert.Equal で値を確かめていた最初のテストより、分岐カバレッジは高くなります。この例を Coverlet で測ると、分岐カバレッジは最初のテストの50%から75%に上がりました。それでも、Subtotal が掛け算をまちがえて足し算をしていても、このテストは失敗しません。
flowchart TB
accTitle: 実行されたことと確かめたことの違い
accDescr: カバレッジは実行されたかだけを数え、結果を確かめたかはアサーションの有無で決まるので、アサーションのないテストでもカバレッジは上がるが誤りは見つからないことを示す。
T["テストがコードを実行する"] --> C["カバレッジが上がる"]
T --> A{"結果をアサーションで確かめているか"}
A -->|"確かめている"| D["誤りがあればテストが失敗する"]
A -->|"確かめていない"| E["誤りがあってもテストは通る"]
図5: カバレッジは実行されたことしか数えないので、結果を確かめないテストでも割合は上がる。
Google のテストブログの記事も、カバレッジの割合が高いことはテストの品質が高いことを保証しないとし、テストが実行した行を十分に確かめているかを評価するにはミューテーションテストのほうがよいと書いています4。同じ記事は、割合が低いことは製品の広い範囲が自動テストで一度も実行されていないことを意味する、とも書いています4。割合は高いときより低いときのほうがはっきりした情報になります。
テストが結果を確かめているかは、シリーズの次の記事「ミューテーションテストでテストをテストする」で扱います。
自動生成されたコードを計測から外す
ツールが自動生成したコード(データベースのマイグレーション、Windows フォームのデザイナーが生成するコードなど)が計測の対象に入っていると、自分たちが書いたコードの未実行の行が埋もれます。こうしたコードは計測から外せます。
ExcludeFromCodeCoverageAttribute を付けたクラスやメソッドは、カバレッジの計測から外れます。クラスや構造体に付けると、そのメンバー全部が外れます6。Justification プロパティに外した理由を書けます6。Coverlet はこの属性の付いたコードを常に外します2。
属性を付けられない生成コードは、Coverlet の設定ファイル(runsettings)で、属性やファイルの名前を指定して外します。
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<Format>cobertura</Format>
<ExcludeByAttribute>GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute>
<ExcludeByFile>**/Migrations/*.cs</ExcludeByFile>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
dotnet test --collect:"XPlat Code Coverage" --settings coverlet.runsettings
GeneratedCodeAttribute と CompilerGeneratedAttribute は、Coverlet が既定で外す属性ではありません。外したいときは、このように ExcludeByAttribute に書きます2。ExcludeByFile のパスは例なので、自分のプロジェクトで生成コードが置かれる場所に合わせます。
外す設定は割合を上げるためではなく自分たちが書いたコードを見やすくするために使います。手で書いたロジックを外すと、テストされていないことがレポートから見えなくなります。外したものと理由は Justification や設定ファイルのコメントに残しておきます。
flowchart TB
accTitle: 計測から外すかどうかの判断
accDescr: ツールが生成したコードは属性か設定ファイルで計測から外し、手で書いたロジックは外さずにレポートに残し、外したものと理由を記録することを示す。
A["計測の対象のコード"] --> B{"ツールが生成したコードか"}
B -->|"はい"| C["属性か設定ファイルで外す"]
B -->|"いいえ(手で書いたロジック)"| D["外さずにレポートに残す"]
C --> E["外したものと理由を残す"]
図6: 計測から外すのは生成コードに限り、外した理由を残す。
変更した行だけのカバレッジを見る
大きなプロジェクトでは、全体の割合を見ても、今回の変更にテストがあるかは分かりません。AIが書いた変更を受け入れるかを決めるときは、その変更で追加・修正された行だけのカバレッジを見ると、確かめるべき範囲が絞れます。
diff-cover はカバレッジのレポートと git diff の出力を比べて、変更した行のカバレッジを報告するツールです7。Cobertura 形式の XML を読めるので、Coverlet の出力をそのまま渡せます。既定では origin/main と比べ、別のブランチと比べるときは --compare-branch を指定します。--fail-under を付けると、割合がその値に届かないときに0以外の終了コードで終わります7。
pip install diff-cover
diff-cover TestResults\{guid}\coverage.cobertura.xml --compare-branch=origin/main
flowchart TB
accTitle: 変更した行だけのカバレッジ
accDescr: カバレッジのレポートとgit diffの変更行を突き合わせ、変更した行のうち実行されていない行を一覧にし、その行にテストを足すかをレビューで判断する流れを示す。
A["カバレッジのレポート"] --> C["変更した行と突き合わせる"]
B["git diff の変更行"] --> C
C --> D["変更した行のうち実行されていない行"]
D --> E["レビューでテストを足すかを判断する"]
図7: 変更した行に絞ると、今回の変更でテストが足りない場所が分かる。
Google のテストブログの記事は、プロジェクト全体で90%を超える目標はおそらく割に合わないとする一方、変更ごとのカバレッジの目標は99%が妥当で、90%がよい下限だと書いています4。この数字は Google の目安です。自分たちの値を決めるときも、まずはレビューで未実行の行を読む使い方から始め、数字で機械的に止めるのは後にするほうが、割合を目的にしない運用を保ちやすくなります。
関連記事
参考リンク
-
Microsoft Learn, Use code coverage for unit testing. コードカバレッジの定義、データコレクターとレポートジェネレーター、
dotnet test --collect:"XPlat Code Coverage"、Cobertura 形式の出力、ReportGenerator の使い方。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 -
Coverlet, VSTest Integration. runsettings での設定、既定で外す属性、MTP v2 との互換性。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Microsoft.Testing.Platform (MTP) code coverage.
Microsoft.Testing.Extensions.CodeCoverageとcoverlet.MTPのオプション。 ↩ ↩2 -
Google Testing Blog, Code Coverage Best Practices. 割合と品質の関係、ミューテーションテスト、変更ごとのカバレッジの目安、コードレビューでの使い方。 ↩ ↩2 ↩3 ↩4
-
Martin Fowler, TestCoverage. カバレッジはテストされていない部分を見つける道具であり、テストの良さの数字ではないこと。 ↩ ↩2
-
Microsoft Learn, ExcludeFromCodeCoverageAttribute Class. 属性の対象と
Justificationプロパティ。 ↩ ↩2 -
diff-cover, README. カバレッジのレポートと
git diffを比べて変更した行のカバレッジを報告するツール。 ↩ ↩2
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
ミューテーションテストでテストをテストする ── Stryker.NETで確かめていないテストを見つける
コードをわざと少し変えて、テストが失敗するかを見るのがミューテーションテストです。実行しているのに何も確かめていないテストの見つけ方を、Stryker.NETの導入、レポートの読み方、生き残ったミュータントの扱いまで説明します。
プロパティベーステストで、例では届かない入力を試す ── FsCheckとxUnitによるC#の実践
AIが書いたテストは、書いた側が思いついた入力しか確かめません。どの入力でも成り立つ性質を書き、入力をFsCheckに作らせるプロパティベーステストを、C#とxUnitのコードで説明します。
AIが書いたテストを信用してよいか ── 通るのに何も確かめないテストの見分け方
AIが書いたテストは、通っていても何も確かめていないことがあります。アサーションの不足、実装から写した期待値、代役の使いすぎ、正常系だけのテスト、不具合に合わせた書き換えの見分け方と、道具での補い方をまとめます。
業務アプリの日時とタイムゾーン ── 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 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
カバレッジの測り方やレポートの読み方は、テストの書き方やレビューの進め方と合わせて決めると効果が出やすいので、技術相談・設計レビューとして扱いやすい題材です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- コードカバレッジとは何ですか?
- テストを実行したときに、プログラムのどれだけの部分が実行されたかを表す値です。行・分岐・メソッドの単位で測ります。実行されたかどうかだけを数えるので、テストが結果を正しく確かめているかは分かりません。
- 行カバレッジと分岐カバレッジは何が違いますか?
- 行カバレッジはその行が1回でも実行されたかを数え、分岐カバレッジは if や条件演算子の分かれ道のそれぞれを通ったかを数えます。1行に条件が書いてあると、その行は実行済みでも片方の道を一度も通っていないことがあり、分岐カバレッジではそれが見えます。
- C#でカバレッジを測るにはどうすればよいですか?
- VSTest で動くテストプロジェクトなら、coverlet.collector パッケージを入れて dotnet test --collect:"XPlat Code Coverage" を実行すると、TestResults の下に coverage.cobertura.xml ができます。Microsoft.Testing.Platform で動かす場合は、coverlet.MTP か Microsoft.Testing.Extensions.CodeCoverage を使います。HTML のレポートは ReportGenerator で作れます。
- カバレッジは何パーセントを目標にすればよいですか?
- 割合そのものを目標にすることは勧めません。アサーションのないテストでも割合は上がるからです。割合よりも、実行されていない行と分岐を読み、そこにテストが要るかを判断するほうが役に立ちます。変更した行だけのカバレッジを見る方法もあります。