単体テストにコードカバレッジを使用する

重要

この記事では、プロジェクト例の作成について説明します。 既にプロジェクトがある場合は、「コード カバレッジ ツール」セクションに進むことができます。

単体テストは、機能を保証し、リファクタリングの際の検証手段となります。 コードカバレッジとは、単体テストで実行する、行、分岐、またはメソッドのいずれかのコード量の尺度です。 たとえば、条件分岐が (分岐 a分岐 b の) 2 つしかない単純なアプリケーションのコードで、条件付き分岐 a を単体テストで検証する場合、分岐のコードカバレッジは 50% と報告されます。

この記事では、Coverlet での単体テストでのコードカバレッジの用途と、ReportGenerator でのレポートの生成について説明します。 この記事では、テスト フレームワークとして C# と xUnit を使用していますが、MSTest と NUnit のいずれも使用することが可能です。 Coverlet とは、C# 用のクロスプラットフォームのコード カバレッジ フレームワークである、GitHub 上のオープン ソース プロジェクトです。 Coverlet は .NET Foundation に含まれています。 Coverlet は、レポートの生成に使用する Cobertura のカバレッジのテストの実行データを収集します。

この記事では、Coverlet のテストの実行から収集したコードカバレッジ情報を使用して、レポートを生成する方法についても詳しく説明します。 レポートは、別の GitHub 上のオープンソース プロジェクトである ReportGenerator を使用して生成できます。 ReportGenerator を使用すると、特に Cobertura から生成されたカバレッジ レポートが、人間にとって判読可能なさまざまな形式のレポートに変換されます。

この記事は、サンプル ブラウザーで使用できる、サンプル ソース コード プロジェクトに基づいています。

テスト対象のシステム

"テスト対象のシステム" とは、単体テストを記述している対象のコードを指します。これは、オブジェクト、サービス、またはその他のテスト可能な機能を公開しているものです。 この記事では、テスト対象のシステムとなるクラス ライブラリと、それに対応する 2 つの単体テスト プロジェクトを作成します。

クラス ライブラリを作成する

コマンド プロンプトで、次のように dotnet new classlib コマンドを使用して、UnitTestingCodeCoverage という名前の新しいディレクトリに、新しい .NET Standard クラス ライブラリを作成します。

dotnet new classlib -n Numbers

以下のスニペットでは、数値が素数であるかどうかを確認する単純な PrimeService クラスを定義しています。 以下のスニペットをコピーし、Numbers ディレクトリに自動作成された Class1.cs の内容と置き換えます。 Class1.cs ファイルを PrimeService.cs に名前変更します。

namespace System.Numbers
{
    public class PrimeService
    {
        public bool IsPrime(int candidate)
        {
            if (candidate < 2)
            {
                return false;
            }

            for (int divisor = 2; divisor <= Math.Sqrt(candidate); ++divisor)
            {
                if (candidate % divisor == 0)
                {
                    return false;
                }
            }
            return true;
        }
    }
}

ヒント

Numbers クラス ライブラリが意図的に System 名前空間に追加されていることに着目してください。 これにより、using System; 名前空間を宣言せずに System.Math にアクセスできます。 詳細については、「namespace (C# リファレンス)」を参照してください。

テスト プロジェクトを作成する

次のように、dotnet new xunit コマンドを使用して、同じコマンド プロンプトから 2 つの新しい xUnit テスト プロジェクト (.NET Core) テンプレートを作成します。

dotnet new xunit -n XUnit.Coverlet.Collector
dotnet new xunit -n XUnit.Coverlet.MSBuild

新しく作成した 2 つの xUnit テスト プロジェクトには、Numbers クラス ライブラリのプロジェクト参照が追加される必要があります。 これにより、テストのためにテスト プロジェクトが PrimeService にアクセスできるようになります。 コマンド プロンプトで、次のように dotnet add コマンドを使用します。

dotnet add XUnit.Coverlet.Collector\XUnit.Coverlet.Collector.csproj reference Numbers\Numbers.csproj
dotnet add XUnit.Coverlet.MSBuild\XUnit.Coverlet.MSBuild.csproj reference Numbers\Numbers.csproj

coverlet.msbuild NuGet パッケージに依存して、MSBuild プロジェクトに適切な名前が付けられます。 次のように dotnet add package コマンドを実行し、このパッケージの依存関係を追加します。

cd XUnit.Coverlet.MSBuild && dotnet add package coverlet.msbuild && cd ..

前のコマンドでは、ディレクトリを効果的に MSBuild テスト プロジェクトに変更し、NuGet パッケージを追加しました。 これが完了すると、1 レベル上がり、ディレクトリが変更されます。

両方の UnitTest1.cs ファイルを開き、その内容を次のスニペットと置き換えます。 UnitTest1.cs ファイルを PrimeServiceTests.cs に名前変更します。

using System.Numbers;
using Xunit;

namespace XUnit.Coverlet
{
    public class PrimeServiceTests
    {
        readonly PrimeService _primeService;

        public PrimeServiceTests() => _primeService = new PrimeService();

        [Theory]
        [InlineData(-1), InlineData(0), InlineData(1)]
        public void IsPrime_ValuesLessThan2_ReturnFalse(int value) =>
            Assert.False(_primeService.IsPrime(value), $"{value} should not be prime");

        [Theory]
        [InlineData(2), InlineData(3), InlineData(5), InlineData(7)]
        public void IsPrime_PrimesLessThan10_ReturnTrue(int value) =>
            Assert.True(_primeService.IsPrime(value), $"{value} should be prime");

        [Theory]
        [InlineData(4), InlineData(6), InlineData(8), InlineData(9)]
        public void IsPrime_NonPrimesLessThan10_ReturnFalse(int value) =>
            Assert.False(_primeService.IsPrime(value), $"{value} should not be prime");
    }
}

ソリューションの作成

コマンド プロンプトから、クラス ライブラリと 2 つのテスト プロジェクトをカプセル化する新しいソリューションを作成します。 dotnet sln コマンドの使用する場合、次のようになります。

dotnet new sln -n XUnit.Coverage

これにより、UnitTestingCodeCoverage ディレクトリに XUnit.Coverage という新しいソリューション ファイル名が作成されます。 ソリューションのルートにプロジェクトを追加します。

dotnet sln XUnit.Coverage.sln add **/*.csproj --in-root

次のように dotnet build コマンドを使用してソリューションを作成します。

dotnet build

ビルドが成功した場合は、3 つのプロジェクトが作成され、プロジェクトとパッケージが正しく参照され、ソース コードが正しく更新されます。 お疲れさまでした。

コード カバレッジ ツール

コード カバレッジには、次の 2 種類のツールがあります。

  • DataCollector: DataCollector は、テストの実行を監視し、テストの実行に関する情報を収集します。 これらでは、XML や JSON などのさまざまな出力形式で、収集された情報を報告します。 詳細については、初めての DataCollector の使用に関する記事を参照してください。
  • レポート ジェネレーター: テストの実行から収集したデータを使って、多くの場合スタイル付きの HTML でレポートを生成します。

このセクションでは、データ コレクター ツールに重点を置いて説明します。

.NET にはコード カバレッジ データ コレクターが組み込まれており、Visual Studio でも使用できます。 このデータ コレクターは、Visual Studio でレポートを生成するために使用できるバイナリ .coverage ファイルを生成します。 バイナリ ファイルは人間が判読できないため、Visual Studio の外部でレポートを生成するために使うには、事前に人間が判読できる形式に変換する必要があります。

ヒント

dotnet-coverage ツールは、バイナリ カバレッジ テスト結果ファイルを人間が判読できる形式に変換するために使用できるクロスプラットフォーム ツールです。 詳細については、dotnet-coverage に関するページを参照してください。

Coverlet は、組み込みのコレクターに対するオープンソースの代替手段です。 テスト結果は人間が判読できる Cobertura XML ファイルとして生成され、HTML レポートの生成に使用することができます。 コードカバレッジに Coverlet を使用する場合、既存の単体テスト プロジェクトにパッケージに対する正しい依存関係が存在している必要があります。または .NET グローバル ツールと、それに対応する coverlet.console NuGet パッケージに依存している必要があります。

.NET のテストと統合する

XUnit テスト プロジェクト テンプレートは、既定で coverlet.collector と統合されています。 コマンド プロンプトから、ディレクトリを XUnit.Coverlet.Collector プロジェクトに変更し、次のように dotnet test コマンドを実行します。

cd XUnit.Coverlet.Collector && dotnet test --collect:"XPlat Code Coverage"

Note

"XPlat Code Coverage" 引数は、Coverlet のデータ コレクターに対応するフレンドリ名です。 この名前は必須です。大文字と小文字は区別されません。 .NET の組み込みコード カバレッジ データ コレクターを使うには、"Code Coverage" を使います。

dotnet test の実行の一環で、coverage.cobertura.xml ファイルが結果として TestResults ディレクトリに出力されます。 この XML ファイルにその結果が格納されます。 これは、.NET CLI に依存するクロスプラットフォーム オプションであり、MSBuild を使用できないビルド システムに最適です。

coverage.cobertura.xml ファイルの例は、次のとおりです。

<?xml version="1.0" encoding="utf-8"?>
<coverage line-rate="1" branch-rate="1" version="1.9" timestamp="1592248008"
          lines-covered="12" lines-valid="12" branches-covered="6" branches-valid="6">
  <sources>
    <source>C:\</source>
  </sources>
  <packages>
    <package name="Numbers" line-rate="1" branch-rate="1" complexity="6">
      <classes>
        <class name="Numbers.PrimeService" line-rate="1" branch-rate="1" complexity="6"
               filename="Numbers\PrimeService.cs">
          <methods>
            <method name="IsPrime" signature="(System.Int32)" line-rate="1"
                    branch-rate="1" complexity="6">
              <lines>
                <line number="8" hits="11" branch="False" />
                <line number="9" hits="11" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="7" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="10" hits="3" branch="False" />
                <line number="11" hits="3" branch="False" />
                <line number="14" hits="22" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="57" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="15" hits="7" branch="False" />
                <line number="16" hits="7" branch="True" condition-coverage="100% (2/2)">
                  <conditions>
                    <condition number="27" type="jump" coverage="100%" />
                  </conditions>
                </line>
                <line number="17" hits="4" branch="False" />
                <line number="18" hits="4" branch="False" />
                <line number="20" hits="3" branch="False" />
                <line number="21" hits="4" branch="False" />
                <line number="23" hits="11" branch="False" />
              </lines>
            </method>
          </methods>
          <lines>
            <line number="8" hits="11" branch="False" />
            <line number="9" hits="11" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="7" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="10" hits="3" branch="False" />
            <line number="11" hits="3" branch="False" />
            <line number="14" hits="22" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="57" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="15" hits="7" branch="False" />
            <line number="16" hits="7" branch="True" condition-coverage="100% (2/2)">
              <conditions>
                <condition number="27" type="jump" coverage="100%" />
              </conditions>
            </line>
            <line number="17" hits="4" branch="False" />
            <line number="18" hits="4" branch="False" />
            <line number="20" hits="3" branch="False" />
            <line number="21" hits="4" branch="False" />
            <line number="23" hits="11" branch="False" />
          </lines>
        </class>
      </classes>
    </package>
  </packages>
</coverage>

ヒント

ビルド システムで既に MSBuild を使用している場合は、代わりに MSBuild パッケージを使用することもできます。 コマンド プロンプトで、ディレクトリを XUnit.Coverlet.MSBuild プロジェクトに変更し、次のように dotnet test コマンドを実行します。

dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura

coverage.cobertura.xml ファイルが結果として出力されます。 こちらの MSBuild 統合ガイドに従ってください

レポートの生成

これで、単体テストの実行からデータを収集できるようになったので、ReportGenerator を使用してレポートを生成できます。 次のように dotnet tool install コマンドを使用し、.NET グローバル ツールとして ReportGenerator NuGet パッケージをインストールします。

dotnet tool install -g dotnet-reportgenerator-globaltool

ツールを実行し、前のテストの実行の出力である coverage.cobertura.xml ファイルを指定して必要なオプションを指定します。

reportgenerator
-reports:"Path\To\TestProject\TestResults\{guid}\coverage.cobertura.xml"
-targetdir:"coveragereport"
-reporttypes:Html

このコマンドを実行すると、HTML ファイルにレポートが生成されます。

Unit test-generated report

関連項目

次のステップ