[.NET][C#]NUnitを使用して単体テスト自動化　基本編

# はじめに
近々.NET、C#の立ち上げプロジェクトに参画するので.NETについて色々勉強中:muscle:
今回は単体テスト自動化について調べてみた。
.NETではNUnit、xUnit、MSTestの3種類がメジャー？な自動テストFWらしいので
一番初めに目についたNUnitを使用してみることにしました。

# 環境
.NET：3.1.401
C#：8.0
NUnit：3.12.0

# 事前準備
まず、テスト対象のプロジェクトを作成。

```
dotnet new classlib -o Calc
```

今回は下記のような電卓クラスを作成。

```C#
    public static class Calc
    {
        /// <summary>足し算</summary>
        public static int Add(int a, int b) { return a + b; }

        /// <summary>引き算</summary>
        public static int Sub(int a, int b) { return a - b; }

        /// <summary>掛け算</summary>
        public static int Multi(int a, int b) { return a * b; }

        /// <summary>割り算</summary>
        public static int Div(int a, int b) { return a / b; }
        
        /// <summary>除算</summary>
        public static int Mod(int a, int b) { return a % b; }
    }
```

次にテスト用プロジェクトを作成。
プロジェクトテンプレートに**nunit**があるので、それを使用。
Javaとは違いテスト対象プロジェクトとテスト用プロジェクトは分けるものらしい。

```
dotnet new nunit -o Calc.Test
```

次に、以下のコマンドで
テスト対象のプロジェクトが属するソリューションと同じソリューションに設定する。
また、テスト対象プロジェクトを参照できるように設定する。

```
dotnet sln add ./Calc.Test/Calc.Test.csproj
cd Calc.Test
dotnet add reference ../Calc/Calc.csproj
```
これで事前準備は完了。
階層は下記のような感じに。

```
CaclSolution
│  CaclSolution.sln
│
├─Calc
│  │  Calc.cs
│  │  Calc.csproj
│  │
│  └─obj
│      {省略}
│
└─Calc.Test
    │  Calc.Test.csproj
    │  CalcTest.cs
    │
    └─obj
        {省略}
```

# テストメソッド作成
## 1. テストロジック作成
### 1-1. Classic Modelによるアサーション
**Assert**クラスには、テスト対象機能を評価する様々なメソッドが用意されており、
これらを使用してテスト評価対象の機能が想定した結果となるかどうかを評価する。
**Assert#Thatメソッド以外のテスト評価メソッド**を
**Classic Model**と言う。

```C#
        public void AddTest()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.AreEqual(answer, Calc.Add(param1, param2));
        }
```
例えば、上記のような**AreEqual**メソッドを利用の場合、
第1引数に想定される値、
第2引数にテスト対象機能の実行結果を指定すると、
2つの値が一致する場合テスト結果OK、異なる場合テスト結果NGという結果が得られる。

下記のように第3引数にNG時のメッセージを指定することも可能。

```C#
        public void AddTest()
        {
            int param1 = 5;
            int param2 = 11;
            int answer = 15;
            Assert.AreEqual(
                answer,
                Calc.Add(param1, param2),
                $"足し算ロジックNG：param1={param1}, param2={param2}");
        }
```

**Classic Model**のよく使いそうなメソッドは下記。

|Assertメソッド|評価内容|
| ---- | ---- |
|Assert.True(bool x)|xがtrueならテストOK|
|Assert.False(bool x)|xがfalseならテストOK|
|Assert.Null(object x)|xがNullならテストOK|
|Assert.NotNull(object x)|xがNull以外ならテストOK|
|Assert.Zero(数値型 x)|xが0ならテストOK|
|Assert.NotZero(数値型 x)|xが0以外ならテストOK|
|Assert.IsEmpty(IEnumerable x)|xが空ならテストOK(Listなど)|
|Assert.IsNotEmpty(IEnumerable x)|xが空以外ならテストOK(Listなど)|
|Assert.AreEqual(object y, object x)|xがyと等しいならテストOK|
|Assert.AreNotEqual(object y, object x)|xがyと等しくないならテストOK|
基本的に各メソッドの引数に評価対象機能の結果（戻り値）を指定する感じだ。


その他Classic Modelメソッドは下記参照。
[Classic Modelメソッド一覧](https://docs.nunit.org/articles/nunit/writing-tests/assertions/assertion-models/classic.html)
 
### 1-2. Constraint Modelによるアサーション
**Assert#That**でアサーションするやり方。
制約（評価方法）をロジカルに書くことが可能。
第1引数に評価対象、第2引数に制約（評価方法）を記載する。

```C#
        public void AddTest2()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.That(Calc.Add(param1, param2), Is.EqualTo(answer));
        }
```
上記の場合、ClassicModelのときと同じように
テスト評価対象と想定結果が一致しているかを評価できる。

下記のように制約を複数組み合わせることも可能。

```C#
        public void AddTest2()
        {
            Console.WriteLine("hoge");
            int param1 = 5;
            int param2 = 3;
            int top = 4;
            int bottom = 1;
            Assert.That(Calc.Mod(param1, param2), Is.GreaterThan(bottom) & Is.LessThan(top));
        }
```
上記の場合はテスト評価対象の実行結果が1超えかつ4未満であることを評価している。

制約（評価方法）のためのクラスやメソッドは下記を参照。
[Constraint Model](https://docs.nunit.org/articles/nunit/writing-tests/assertions/assertion-models/constraint.html)
[制約一覧](https://docs.nunit.org/articles/nunit/writing-tests/constraints/Constraints.html)

## 2. 注釈付与
テストロジックを実装したメソッドが
テストメソッドであることを認識させるための注釈をメソッドに付ける。

また、テストの事前・事後に処理を行いたい場合、
事前・事後処理用の注釈を実装したメソッドに付与する。

```C#
    public class CalcTest
    {

        [OneTimeSetUp]
        public void Init()
        { /* 事前処理(1回のみ実行) */ }

        [SetUp]
        public void InitMethod()
        { /* テストメソッド事前処理 */ }

        [TestCase]
        public void AddTest()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.AreEqual(nswer, Calc.Add(param1, param2));
        }

        [TearDown] public void CleanupMethod()
        { /* テストメソッド事後処理 */ }

        [OneTimeTearDown]
        public void Cleanup()
        { /* 事後処理(1回のみ実行) */ }
        
    }
}
```
よく使いそうなのは下記。

| 注釈 | 説明 |
| ---- | ---- |
| TestCase | テストメソッドに付ける |
| OneTimeSetUp | 事前処理メソッドに付ける。テスト実行1回につき1回だけ実行される。 |
| SetUp | テストメソッド事前処理メソッドに付ける。テストメソッド実行の度に実行される。 |
| TearDown | テストメソッド事後処理メソッドに付ける。テストメソッド実行の度に実行される。 |
| OneTimeTearDown | 事後処理メソッドに付ける。テスト実行1回につき1回だけ実行される。 |

単体テスト環境でDBを使う場合のコネクション生成・破棄や設定ファイル読み込みなど、
初回と最後に1度だけやっておきたい処理はOneTimeSetUpやOneTimeTearDown に書くといいかも。
テストメソッド間でリセットしておきたいこと（例えばDB更新したのを戻すなど）はSetUpやTearDown に書くとよいと思う。

また、TestCase注釈はテストメソッドの引数にパラメータを指定することができ、
かつ、同一テストメソッドに対して異なるパターンのパラメータを複数指定することができる。

```C#
        [TestCase(5, 10, 15)]
        [TestCase(0, 10, 10)]
        [TestCase(0, 10, 11)]
        public void AddTestParam(int param1, int param2, int answer)
        {
            Assert.AreEqual(answer, Calc.Add(param1, param2));
        }
```
このように指定した場合、AddTestParamテストメソッドが指定したそれぞれのパラメータで3回実行される。
この注釈はかなりあるので、また今度色々試してみたい。

[Attribute一覧](https://docs.nunit.org/articles/nunit/writing-tests/attributes.html)

## 3. テスト実行
ソリューション直下、もしくはプロジェクトフォルダ直下で`dotnet test`を打つと
テストが実行される。

```

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

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

テストの実行に成功しました。
テストの合計数: 1
     成功: 1
合計時間: 0.6627 秒
```
テスト結果NGの場合は、以下のように出力される。(Assert.AreEqualの場合)

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

合計 1 個のテスト ファイルが指定されたパターンと一致しました。
  X AddTest() [37ms]
  エラー メッセージ:
     Expected: 15
  But was:  16

  スタック トレース:
     at Calc.Test.CalcTest.AddTest() in M:\develop\tools\VSCode\VSCodeWorkspace\CaclSolution\Calc.Test\CalcTest.cs:line 24


テストの実行に失敗しました。
テストの合計数: 1
     失敗: 1
合計時間: 0.6800 秒
```

なお、ソリューションフォルダ直下で`dotnet test`を打った場合、
ソリューション配下の全てのプロジェクトのテストが実行される。
テスト実行が特定のプロジェクトだけでよいのであれば、
そのプロジェクト配下に移動して`dotnet test`を打てばいい。


## 4. まとめ
とりあえず.NETにおける単体テスト実装を最低限できるところまではできた。
アサーションのモデルはClassic Modelの方が直感的でわかりやすそうだし、
Constraint Modelだと制約をロジカルに実装するとそこにバグが生まれやすそうではある。
ただ、複数条件でチェックしたいときとかありそうかなぁ。

あと、MicroSoftの単体テストのベスト プラクティスは分かりやすく
テスト実装の標準化に使ってもいいかも。
[.NET Core と .NET Standard での単体テストのベスト プラクティス](https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-best-practices)

Assertメソッド、Attributeは書いたもの以外も沢山あるので
また今度いろいろ試してまとめてみたい。

## 参考
[NUnit公式](https://docs.nunit.org/articles/nunit/intro.html)
[MicroSoft解説ページ](https://docs.microsoft.com/ja-jp/dotnet/core/testing/?pivots=nunit)