C# + xUnitのプロジェクトでユニットテストの結果とテストカバレッジ率をレポートに出力させる。

GxPの@h-uminoue-gxpです。
この記事は[グロースエクスパートナーズ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/gxp)の23日目です。

# 概要
　最近関わったC#案件で実装の傍ら、微力ながらCIの構築(クライアントアプリ案件なのでCDはありません)を担当させていただきました。その時にテスト結果とテストカバレッジをCI用にどうレポートするかを試した結果を書かせていただきます。
　記事のタイトル通り、テストフレームワークは実案件に沿ってxUnitを使用しております。
 
# 前準備(とりあえずテスト回る状態にする)

　例えばこんなクラスがプロダクトコードにあり
```csharp:Calcurator.cs
namespace sample_qiita_2023
{
    public class Calcurator
    {
        public int SumTwoValues(int value1, int value2)
        {
            return value1 + value2;
        }

        public int DiffTwoValues(int value1, int value2)
        {
            return value1 - value2;
        }
    }
}
```

　テストコードがこうあったとします。
```csharp:CalcuratorTests.cs
namespace sample_qiita_2023_test
{
    public class CalcuratorTests
    {
        private readonly Calcurator calcurator;

        public CalcuratorTests()
        {
            calcurator = new Calcurator();
        }

        [Fact]
        public void SumTwoValuesTest()
        {
            Assert.Equal(5, calcurator.SumTwoValues(3, 2));
        }

        [Fact]
        public void DiffTwoValuesTest()
        {
            Assert.Equal(1, calcurator.DiffTwoValues(3, 2));
        }
    }
}
```

　適当過ぎる例ですみませんが、本題の話はこれでできますのでご容赦ください。

　テストエクスプローラーで回してみると通ります(当たり前ですが)。
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/287f0aaa-70d8-7d5b-d7d0-6e2d31468c3e.png)

　Powershellからも回してみます。
```powershell
dotnet test
```

```
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。

  // 中略 //

  Microsoft (R) Test Execution Command Line Tool Version 17.7.2 (x64)
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。

成功!   -失敗:     0、合格:     2、スキップ:     0、合計:     2、期間: 4 ms - sample-qiita-2023-test.dll (net7.0)
```

　問題ありませんのでこれで本題の話をする準備ができました。

# テスト結果のレポート
　ローカルでテスト回すだけならここまでで良いですが、後でCIで使うことを考え、テストを実行した結果をレポートに出力させてみようと思います。レポートツールは[JunitXml.TestLogger](https://www.nuget.org/packages/JunitXml.TestLogger/)を使いました。sample_qiita_2023_testにNugetでインストールしておきます。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/d57a363e-c60d-5f32-873e-196b52684645.png)


 　dotnet testコマンドでロガーにjunitを指定してあげると、指定したファイルパスにテスト結果のxmlが出力されます。

```powershell
dotnet test --logger "junit;LogFilePath=./TestResult/result.xml"
```
```
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。

  // 中略 //

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
Results File: ./TestResult/result.xml

成功!   -失敗:     0、合格:     2、スキップ:     0、合計:     2、期間: 5 ms - sample-qiita-2023-test.dll (net7.0)
```
　無事xmlが生成されています。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/7fe4e05a-ba76-7689-2bdf-a21beef61e60.png)


　これを見やすくするのに[junit2html](junit2html) (https://github.com/inorton/junit2html) がとても便利だったので使わせていただきました(pythonはあらかじめインストールしておきます)。

 ```pip
 pip install junit2html
 ```

 　先ほどのxmlをこれに食わせてあげると見やすくhtmlに変換してくれました。

  ```powershell
  junit2html ./sample-qiita-2023-test/TestResult/result.xml ./sample-qiita-2023-test/TestResult/result.html
  ```

  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/ed76845a-5b9b-0443-d6ce-84c34ed40986.png)


  確認してみます。

  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/c5d73b7f-234a-c7f6-61a8-0cb2170bb055.png)

  良さそうですね。

# カバレッジ率を測る
　続いてユニットテストのカバレッジ率をレポートさせたいと思います。カバレッジ率の測定には[coverlet.collector](https://www.nuget.org/packages/coverlet.collector)を使いました(テストライブラリがxunitの場合、標準でインストールされています)。

 　dotnet test コマンドでテストを回す際、--collect:"XPlat Code Coverage"とオプションを付けると、カバレッジ率が測定されたxmlが出来上がります。

```powershell
dotnet test --collect:"XPlat Code Coverage" --logger "junit;LogFilePath=./TestResult/result.xml"
```
```
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  
  // 中略 //

テスト実行を開始しています。お待ちください...
合計 1 個のテスト ファイルが指定されたパターンと一致しました。
Results File: ./TestResult/result.xml

成功!   -失敗:     0、合格:     2、スキップ:     0、合計:     2、期間: 6 ms - sample-qiita-2023-test.dll (net7.0)

添付ファイル:
  ...\sample-qiita-2023\sample-qiita-2023-test\TestResults\{uuid}\coverage.cobertura.xml
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/7b523cf2-23e6-06c2-cd4d-c73a55d986dc.png)


　これを元にレポートを生成するには[ReportGenerator](https://www.nuget.org/packages/ReportGenerator)を使います。まずインストール。

 ```powershell
 dotnet tool install -g dotnet-reportgenerator-globaltool
 ```

　そして先ほどのxmlを以下のように指定してreportgeneratorのコマンドを実行します。
```powershell
reportgenerator "-reports:.\sample-qiita-2023-test\TestResults\{uuid}\coverage.cobertura.xml;" "-targetdir:coveragereport" "-reporttypes:Html"
```

　出来上がったhtmlを確認します(index.htmlを開いてください)。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/b9d3f2d1-5fc2-0ab5-b211-4121dfd95a97.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/154065d9-a0c3-3a14-fece-9a2777076513.png)
 

　測定してくれました。

 # カバレッジ測定、もうひと手間加える

 ### 指定したネームスペースに属するコードをカバレッジの測定対象から除外する
　さきほどのカバレッジのレポートをよく見ると、テストコードもカバレッジの対象に含まれてしまっています。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/288c0021-5d15-5b30-88b8-3c5a680cc66f.png)

　(※この現象、発生しないこともあります。上のサンプルでは発生しませんでしたが例示のために後述のrunsettingsを使って無理矢理発生させました)

　また、何らかのライブラリを使用している場合、そのライブラリが生成したコードもカバレッジに含まれてしまう場合があります。試しに上記のCalcuratorクラスをDryIocを使ってDIさせてみた上で、カバレッジを出させてみます。

```csharp:ICalcurator.cs
namespace sample_qiita_2023
{
    public interface ICalcurator
    {
        public int SumTwoValues(int value1, int value2);

        public int DiffTwoValues(int value1, int value2);
    }
}
```
```csharp:Calcurator.cs
namespace sample_qiita_2023
{
    public class Calcurator : ICalcurator
    {
        public int SumTwoValues(int value1, int value2)
        {
            return value1 + value2;
        }

        public int DiffTwoValues(int value1, int value2)
        {
            return value1 - value2;
        }
    }
}
```
 ```csharp:CalcuratorTests.cs
using DryIoc;
using sample_qiita_2023;

namespace sample_qiita_2023_test
{
    public class CalcuratorTests
    {
        private readonly ICalcurator calcurator;

        public CalcuratorTests()
        {
            var container = new Container();
            container.Register<ICalcurator, Calcurator>(Reuse.Singleton);
            calcurator = container.Resolve<ICalcurator>();
        }

        [Fact]
        public void SumTwoValuesTest()
        {
            Assert.Equal(5, calcurator.SumTwoValues(3, 2));
        }

        [Fact]
        public void DiffTwoValuesTest()
        {
            Assert.Equal(1, calcurator.DiffTwoValues(3, 2));
        }
    }
}
```
　この状態でカバレッジを出力すると

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/0c31b10e-b51c-c268-8917-76dbac7aba3e.png)

 
　ご覧の通りDryIocのコードが含まれてしまっています。

 　指定したネームスペースのコードをカバレッジの測定対象から除外できればこれらは解決します。そのために設定ファイルを作ります(拡張子は.runsettings)。

```xml:test.runsettings:
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <!-- Configurations for data collectors -->
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat Code Coverage">
        <Configuration>
          <Format>cobertura</Format>
          <Exclude>[sample_qiita_2023_test.*]*,[*]DryIoc.*</Exclude>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>
```

　Excludeでsample_qiita_2023_testプロジェクトの全コードと、全プロジェクトのDryIocネームスペースの全コードを除外しています。

　dotnet testコマンドを打つ際にこの設定ファイルを使用するように指定すると、この設定に応じてカバレッジファイルを作成してくれます。

 ```powershell
 dotnet test -s test.runsettings --logger "junit;LogFilePath=./TestResult/result.xml"
 ```

　後は先ほどと同じようにレポートのhtmlを生成して中身を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/6e1db948-c820-920b-acd7-7768120dc1da.png)


　無事、指定したコードがカバレッジ測定の対象から除外されました。

### 指定したクラスやメソッドをカバレッジの測定対象から除外する
　実際のプロダクトでは「テストの書きようがない、あるいは書いてもしょうがない(無意味)」というクラスやメソッドもあると思います。それらも測定の対象から除外できるとなお良いです。
　それには、除外したいクラスやメソッドに[ExcludeFromCodeCoverage]属性を付与します。

　試しに、CalcuratorのDiffTwoValuesとProgramクラスに[ExcludeFromCodeCoverage]を付与してみます。
```csharp:program.cs
namespace sample_qiita_2023
{
    [ExcludeFromCodeCoverage]
    internal class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");
        }
    }
}
```
```csharp:Calcurator.cs
namespace sample_qiita_2023
{
    public class Calcurator : ICalcurator
    {
        public int SumTwoValues(int value1, int value2)
        {
            return value1 + value2;
        }

        [ExcludeFromCodeCoverage]
        public int DiffTwoValues(int value1, int value2)
        {
            return value1 - value2;
        }
    }
}
```

　そして、.runsettingsファイルにExcludeByAttribute要素を追加し、対象にExcludeFromCodeCoverageAttributeを含めます(こちらは末尾にAttributeが必要なことに注意)。

　ついでに除外対象の属性をいくつか追加しました。
```xml:test.runsettings:
<?xml version="1.0" encoding="utf-8"?>
<RunSettings>
  <!-- Configurations for data collectors -->
  <DataCollectionRunSettings>
    <DataCollectors>
      <DataCollector friendlyName="XPlat Code Coverage">
        <Configuration>
          <Format>cobertura</Format>
          <Exclude>[sample_qiita_2023_test.*]*,[*]DryIoc.*</Exclude>
          <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
        </Configuration>
      </DataCollector>
    </DataCollectors>
  </DataCollectionRunSettings>
</RunSettings>
```

　これで指定のネームスペースを除外したときのように測定結果を確認してみます。

　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/185faa57-7804-f0d0-e2f4-5309246d6eb0.png)
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2982759/7031719f-6d7b-6d21-07d5-d60e41aa81bf.png)


　目論見通り、CalcuratorのDiffTwoValuesとProgramクラスが除外され、カバレッジ率100%になりました。

# あとがき
　ここまでできたらあとは上で使ってきたコマンドをお使いのCIツールで実行して出来たレポートファイルをアーティファクトとしてアップロードするように設定すれば完了です。参考までに筆者のプロダクトではCIツールはCircleCIを使用しました。理由としては既に弊社内で利用実績があり、すぐに使える状態だったのと、プロジェクトの都合によりLinuxでのビルドが通らないため、Windowsランナーが必要だった、等の理由によります(もっとも、上の簡単なサンプルならLinuxビルドでも多分通るでしょうが)。他のCIツールでもやることは同じですので何かの参考になれば幸いです。