[.NET][C#]NUnitを使用した単体テストのカバレッジレポートを自動生成する

# はじめに
前回、NUnitによる単体テスト自動化方法を学んだので、
今回は実行したNUnitテストのコードカバレッジレポートを自動生成してみる。

【前回】
[[.NET][C#]NUnitを使用して単体テスト自動化　基本編](https://qiita.com/suganury/items/d255ae140373af7d0146)

# (テスト)コードカバレッジとは
> コードカバレッジとは、単体テストで実行する、行、分岐、またはメソッドのいずれかのコード量の尺度です。 たとえば、条件分岐が (分岐 a と_分岐 b_ の) 2 つしかない単純なアプリケーションのコードで、条件付き_分岐 a_ を単体テストで検証する場合、分岐のコードカバレッジは 50% と報告されます。

[MicroSoft 単体テストにコードカバレッジを使用する](https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-code-coverage)　より引用

つまり、テスト実行によってテスト対象のコードを
どれだけ網羅できたかを示す。
これによりテスト抜け漏れのコードが無いかを検知できる。

ただし、あくまでコードの網羅率を示すだけのものであり、
機能の正しさ、テストの正しさを完全に保証するものではない。
例えば引数の四捨五入結果を返すメソッドに引数として1と9を渡すテストを行えば、
切り捨て・繰り上げのすべてのコードを網羅するだろうが、
その境目の判定が想定通り機能するかはテストされていない。
コードカバレッジが100%だからといって完全にバグを取り除けたということは無いということを念頭に置く。

# 実施環境
.NET：3.1.401
C#：8.0
NUnit：3.12.0
Coverlet：2.9.0
ReportGenerator：4.6.7

今回、テストデータコレクターとしてCoverlet、
レポートジェネレータとしてReportGeneratorを使用する。


# 事前準備

## 1.プロジェクト作成
まず、テスト対象のコードを持つプロジェクトと
テスト用プロジェクトを作成する。

```
NUnitTestCoverage
│  NUnitTestCoverage.sln
│
├─TargetLib
│  │  TrafficLight.cs
│  │  TargetLib.csproj
│  │
│  └─obj
│      {省略}
│
└─TargetLibTest
    │  TargetLibTest.csproj
    │  TrafficLightTest.cs
    │
    └─obj
        {省略}
```
`TargetLib`がテスト対象のプロジェクトで
`TargetLibTest`がテスト用のプロジェクト。
`TargetLibTest`から`TargetLib`が参照できるようにしておく。

## 2.テスト用ツールの追加
テスト用プロジェクトにCocerletパッケージを追加する。
`TargetLibTest`プロジェクト配下に移動し、以下のコマンドを実行する。

```
dotnet add package coverlet.msbuild
```

すると、TargetLibTest.csprojに以下の依存関係の設定が追加される。

```
<PackageReference Include="coverlet.msbuild" Version="2.9.0">
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
  <PrivateAssets>all</PrivateAssets>
</PackageReference>
```

次にReportGeneratorをインストールする。
以下のコマンドを実行。

```
dotnet tool install --global dotnet-reportgenerator-globaltool --version 4.6.7
```
コマンドラインで`reportgenerator`コマンドを打ってヘルプが表示されればOK。


## 3.テスト対象クラス作成
今回は下記のような信号機クラスを作成した。

```C#
    public class TrafficLight
    {
        public enum Color {BLUE, YELLOW, RED}

        public static string judge(Color color) {

            switch(color) {
                case Color.BLUE:
                    return "進め！";
                case Color.YELLOW:
                    return "注意して進め！";
                case Color.RED:
                    return "止まれ！";
                default:
                    throw new ArgumentOutOfRangeException();
            }

        }
    }
```

# テスト実施

## 1.テストメソッド作成
まずは、テスト用プロジェクトに
以下のようなテストクラスを作成する。

```C#
using NUnit.Framework;
using TargetLib;
using static TargetLib.TrafficLight.Color;

namespace TargetLibTest
{
    public class TrafficLightTest
    {
        [TestCase(BLUE)]
        public void TrafficLight_judge_success(TrafficLight.Color color)
        {
            string result = TrafficLight.judge(color);
            Assert.AreEqual("進め！", result);
        }
    }
} 
```
このテスト内容でテストを実施、およびテスト結果情報を収集してみよう。

## 2. テスト実施、結果収集
テスト用プロジェクト配下で以下のコマンドを実行する。

```
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura /p:CoverletOutput=./TestResults/20201003_120000/NUnitTestResult.xml
```
オプションの`/p:CoverletOutput`にはテスト結果収集ファイルを保存する場所、ファイル名を指定する。
存在しないフォルダは自動的に生成される。

今回は`./TestResults/{テスト実行日時(yyymmdd_hhmiss)}/NUnitTestResult.xml`を指定した。
単体テストは何度も実施することになるし、後々のテスト自動化のことを考えると
日時を含むフォルダ名を含めた方が良いだろう。

上記コマンドを実行すると下記のようなメッセージが表示される。

```
M:\develop\tools\VSCode\VSCodeWorkspace\NUnitTestCoverage\TargetLibTest\bin\Debug\netcoreapp3.1\TargetLibTest.dll(.NETCoreApp,Version=v3.1) のテスト実行
Microsoft (R) Test Execution Command Line Tool Version 16.7.0
Copyright (c) Microsoft Corporation.  All rights reserved.

テスト実行を開始しています。お待ちください...

合計 1 個のテスト ファイルが指定されたパターンと一致しました。

テストの実行に成功しました。
テストの合計数: 1
     成功: 1
合計時間: 0.6666 秒

Calculating coverage result...
  Generating report '.\TestResults\20201003_120000\NUnitTestResult.xml'

+-----------+--------+--------+--------+
| Module    | Line   | Branch | Method |
+-----------+--------+--------+--------+
| TargetLib | 57.14% | 25%    | 100%   |
+-----------+--------+--------+--------+

+---------+--------+--------+--------+
|         | Line   | Branch | Method |
+---------+--------+--------+--------+
| Total   | 57.14% | 25%    | 100%   |
+---------+--------+--------+--------+
| Average | 57.14% | 25%    | 100%   |
+---------+--------+--------+--------+
```
この時点である程度のカバレッジ情報を把握することができる。

## 3. レポートの生成
次に以下のコマンドでカバレッジレポートを生成する。

```
reportgenerator -reports:./TestResults/20201003_120000/NUnitTestResult.xml -targetdir:./TestResults/20201003_120000 -reporttypes:Html
```
オプションの`-reports`には先ほどテスト実施時に出力したテスト結果収集ファイルを指定、
`-targetdir`にはレポート出力先のディレクトリを指定する。
テスト結果収集ファイルと同じ場所を指定しているが、違う場所を指定してもよい。

コマンドを実行すると指定したフォルダ配下に以下のようにHTML等のファイル群が生成される。
![スクリーンショット 2020-10-03 122111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712352/d58fb75f-4d72-5973-1585-0be341401ad2.png)
これでめでたくレポート生成ができた。
早速`index.html`を開いてみるとこんな感じだ。

![スクリーンショット 2020-10-03 122609.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712352/9e35e053-6539-a10d-8121-c0f0de60d685.png)
テスト対象クラスの行数カバレッジ率や分岐カバレッジ率がパっと見でわかるようになっている。
これはいい:relaxed:
下の方にある表の一番左の列の対象クラス名のリンクをクリックすると
対象クラスのカバレッジの詳細が見ることができる。
![スクリーンショット 2020-10-03 123630.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712352/66300dd8-c982-83ac-93af-ce1f608119e7.png)
コードのどの部分がテストによって実行されたかが一目で分かる。（背景が緑部分が実行された箇所）
コードの左側の数値は実行回数（判定回数？）を表している。

今回はjudgeメソッドに引数をColor.BLUEだけ指定するテストしかないのでこのような結果となった。
試しにテストコードを変えて、全コードを通るようにしてみよう。

* テストコード

```C#
        [TestCase(BLUE, "進め！")]
        [TestCase(YELLOW, "注意して進め！")]
        [TestCase(RED, "止まれ！")]
        public void TrafficLight_judge_success(TrafficLight.Color color, string expectedResult)
        {
            string result = TrafficLight.judge(color);
            Assert.AreEqual(expectedResult, result);
        }
        
        [TestCase((TrafficLight.Color)int.MaxValue)]
        public void TrafficLight_judge_failure(TrafficLight.Color color)
        {
            Assert.Throws<ArgumentOutOfRangeException>(() => TrafficLight.judge(color));
        }
```

* カバレッジレポート

![スクリーンショット 2020-10-03 124527.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712352/8f67bb1b-a7eb-151b-747e-2538c482a3a9.png)
おぉー:grin:
行数・分岐カバレッジ率が両方とも100%になった。


# その他
```C#
        public static string judge2(Color color) {
            if(color.Equals(Color.BLUE) || color.Equals(Color.YELLOW)) {
                return "進んでヨシ！";
            }
            return "ダメ";
        }
```
上記のようなテスト対象のコードがあって

```C#
        [TestCase(BLUE, "進んでヨシ！")]
        [TestCase(RED, "ダメ")]
        public void TrafficLight_judge2_success(TrafficLight.Color color, string expectedResult)
        {
            string result = TrafficLight.judge2(color);
            Assert.AreEqual(expectedResult, result);
        }
```

テストコードを上記のようにした場合、
カバレッジ率はどうなるんだろうか？
![スクリーンショット 2020-10-03 130245.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/712352/b74f8702-c145-9f03-6181-b4b325194467.png)
結果はカバレッジ率100%になった。
**条件付き論理 OR 演算子→「||」**の場合、左側がtrueの場合、右側の式は評価されない。
今回の場合、10行目の`color.Equals(Color.YELLOW)`は評価されていないので
分岐カバレッジ率100%になって欲しくなかったところだが、100%になるようだ。

ひょっとしたらCoverletオプションでなんとかできるかも？
そこらへんは今度調べておきたい。

# まとめ
NUnitでカバレッジレポートまで作成できた。
テスト用のツールをインストール・設定しておけばコマンドラインで実行できるので
テスト実行用のシェルを作っておけば実装の度にパパっとテスト実行・レポート生成まで実施できる。
CD/CIではこれらを利用する感じになるんだろうか？
プロジェクト立ち上げの標準化に関わることが多いので
DepOps系の知識を身に着けていきたいですな

# 参考
[Coverlet(GitHub)](https://github.com/coverlet-coverage/coverlet)
[ReportGenerator(GitHub)](https://github.com/danielpalme/ReportGenerator)