C# visual studioでMSTestを使ったユニットテスト

ソフトウェアテストにおけるユニットテストについて :eyes:

## ユニットテストとは
ユニットテストの特徴
- 関数やメソッド等の個々のユニットを対象としたテスト
- 自動化され、コードを変更した際に繰り返し実行される
- 変更による不具合を検知（ふるまいの違い、デグレ防止）
- 保守時のバグによるリスクが減少する
- 単体テストとも言う

ユニットテストは、開発者が行う最小単位に対するテスト

## visual stuidoのテストツール
 Enterprise Edition 限定のテストツール
- [Live Unit Testing](https://learn.microsoft.com/ja-jp/visualstudio/test/live-unit-testing-start?view=vs-2022) : ユニットテストをリアルタイムで定期的に実行
- [IntelliTest](https://learn.microsoft.com/ja-jp/visualstudio/test/intellitest-manual/?view=vs-2022) : ユニットテストを自動生成
- コードカバレッジ : ユニットテストの割合を調べる

## テストフレームワーク
代表的なテストフレームワークは、以下の3つ
- [MSTest](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-mstest-intro) : visual studio に統合されたMicrosoft製のテストフレームワーク
- [xUnit.net](https://xunit.net/) : OSS、クロスプラットフォームのテストフレームワーク
- [NUnit](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-with-nunit) : xUnitと同様にOSS、クロスプラットフォーム対応。JUnitに近い

`MSTest`は、visual studioに組み込まれているので導入の敷居が低い。また、V2では他と同様にOSSとなり、クロスプラットフォームでの開発が可能となった。

### Star の比較
GitHubのStarを比較すると、xUnitが一番多い :star:

xUnit.net 4k > NUnit 2.5k > MSTest 651

※ 2024.5.7現在


## テストプロジェクト作成
visual studioでは、テストコードを別プロジェクトとして作成する
`MSTest`を例にテストプロジェクトの作成方法を確認する

### .NET Framework の場合
ユニットテストを実装したいメソッドを右クリックして、[単体テストの作成]をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/8b0ee28d-a7a9-388c-cae6-3c478007e26e.png)

単体テストの作成ウィンドウが開くので、OKをクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/2c44d55c-a95b-e593-aeaa-24c3d4f8ecb2.png)

`MSTest`のテストプロジェクトが作成される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/9ad71e5e-408d-cfaf-13c8-0bf6dfb0b7b8.png)

作成されたプロジェクトには、先ほど右クリックしたメソッドのテストコードが自動生成される
```cs
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ConsoleDotNetFramework.UnitTest.Tests
{
    [TestClass]
    public class CalcTests
    {
        [TestMethod]
        public void AddTest()
        {
            Assert.Fail();
        }
    }
}
```

### .NET Core の場合
ソリューションを右クリックして、[追加]→[新しいプロジェクト]をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/e656f0b1-d968-023a-5119-bb113d6ee703.png)

`MSTestテストプロジェクト`を選択して次へ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/054b7567-4b09-de08-d184-e593014041d0.png)

作成したテストプロジェクトの依存関係を右クリックして、[プロジェクト参照の追加]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/e9cd1764-ee3e-ee3d-185d-349fcda142c5.png)

テスト対象のプロジェクトにチェックを入れる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/966baaa8-0940-7272-8f5a-5125129ef8a2.png)

これでテストプロジェクトの準備が完了 :sunny:

## ユニットテストの実行
ユニットテストのクラスには、`[TestClass]`属性（アノテーション）を付与。
テストメソッドには、`[TestMethod]`属性を付与することで、テストフレームワークにユニットテストであることを認識させる
※`()`は無くても動作した
```cs
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ConsoleDotNetFramework.UnitTest.Tests
{
    [TestClass()]
    public class CalcTests
    {
        [TestMethod()]
        public void Add_Number_ReturnsTotalNumber()
        {
            // Arrange
            var calc = new Calc();

            // Act
            var actual = calc.Add(2, 3);

            // Assert
            Assert.AreEqual(5, actual);
        }
    }
}
```
テストコードが実装できたら、ツールバーの[テスト]→[すべてのテストを実行]を選択するか、`ctr + R`の後に`A`でテストを実行する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/01e172f1-e305-b639-ccff-de33ce327551.png)

テストエクスプローラーが展開し、ユニットテストの実行結果が表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/44ebe979-27de-69b2-5025-7a39afd3a80a.png)

ユニットテスト対象のメソッド側にもテストの実行結果が表示される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/482880/87017773-c3c4-db4a-1668-bd6431e5c280.png)

## Assertクラス
Assertクラスは複数用意されており、テストケースに応じて使い分ける
### Assert Class
[Assert Class](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.testtools.unittesting.assert?view=visualstudiosdk-2022)は、値型、参照型、Null判定、真偽値の判定などが可能
visual studioから`F12`で定義に飛び、メソッドを確認するのが一番わかりやすいと思う

### CollectionAssert Class
[CollectionAssert Class](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.testtools.unittesting.collectionassert?view=visualstudiosdk-2022)は、オブジェクトのコレクションを比較する場合や、コレクションの状態を確認する場合に使用する
要素同士の比較や、要素が含まれているか、Nullが含まれているか、要素がすべて一意であるか等

### StringAssert Class
[StringAssert Class](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.testtools.unittesting.stringassert?view=visualstudiosdk-2022)は、文字列のテストに特化したAssertクラス

https://learn.microsoft.com/ja-jp/visualstudio/test/using-the-assert-classes?view=vs-2022#kinds-of-asserts

## テストメソッド例
テストメソッドのサンプルコードを示す

```cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;

namespace ConsoleDotNetFramework.UnitTest.Tests
{
    [TestClass()]
    public class CalcTests
    {
        /// <summary>
        /// 値の比較
        /// </summary>
        [TestMethod()]
        public void Add_Number_ReturnsTotalNumber()
        {
            var calc = new Calc();

            var actual = calc.Add(2, 3);

            Assert.AreEqual(5, actual);
        }

        /// <summary>
        /// 複数の値を比較
        /// </summary>
        /// <param name="expected"></param>
        /// <param name="a"></param>
        /// <param name="b"></param>
        [DataTestMethod]
        [DataRow(3, 1, 2)]
        [DataRow(5, -1, 6)]
        public void Add_ParamNumbers_ReturnsTotalNumber(
            int expected, int a, int b)
        {
            var calc = new Calc();

            var actual = calc.Add(a, b);

            Assert.AreEqual(expected, actual);
        }

        /// <summary>
        /// 参照元が同じかどうかを比較
        /// </summary>
        [TestMethod]
        public void Compare_SameClass()
        {
            var calc = new Calc();
            
            var calc2 = calc;
            
            Assert.AreSame(calc, calc2);
        }

        /// <summary>
        /// 参照元が異なるかどうかを比較
        /// </summary>
        [TestMethod]
        public void Compare_NotSameClass()
        {
            var calc = new Calc();

            var calc2 = new Calc();            

            Assert.AreNotSame(calc, calc2);
        }

        /// <summary>
        /// コレクションの要素を比較
        /// </summary>
        [TestMethod]
        public void Compare_ListValues()
        {
            var list = new List<int>() { 1, 2 };

            var list2 = new List<int>() { 1, 2 };

            CollectionAssert.AreEqual(list, list2);
        }

        /// <summary>
        /// コレクションの要素を比較（インデックスが異なる場合）
        /// </summary>
        [TestMethod]
        public void Compare_ListValues_differentIndex()
        {
            var list = new List<int>() { 1, 2 };

            var list2 = new List<int>() { 2, 1 };

            CollectionAssert.AreNotEqual(list, list2);
        }

        /// <summary>
        /// コレクションの要素を比較（インデックスが異なる場合）
        /// </summary>
        [TestMethod]
        public void Compare_ListValues_Equivalent()
        {
            var list = new List<int>() { 1, 2 };

            var list2 = new List<int>() { 2, 1 };

            // AreEquivalentは、インデックスが異なる場合でもテスト成功とする
            CollectionAssert.AreEquivalent(list, list2);
        }

        /// <summary>
        /// コレクションの要素を比較（一意かどうか）
        /// </summary>
        [TestMethod]
        public void Compare_ListValues_IsUnique()
        {
            var list = new List<int>() { 1, 2 };
            
            CollectionAssert.AllItemsAreUnique(list);
        }

        /// <summary>
        /// コレクションの要素を比較（含まれているか）
        /// </summary>
        [TestMethod]
        public void Compare_ListValues_Contains()
        {
            var list = new List<int>() { 1, 2 };

            CollectionAssert.Contains(list, 1);
        }

        /// <summary>
        /// 例外が発生するかどうかをテスト
        /// </summary>
        [TestMethod]
        public void Throw_InvalidOperationException()
        {
            var calc = new Calc();

            int? num = null;

            // 例外の型を厳密に判断する
            Assert.ThrowsException<InvalidOperationException>(() => calc.Add((int)num, 1));
        }

        /// <summary>
        /// 文字列が含まれているかをテスト
        /// </summary>
        [TestMethod]
        public void Contains_value()
        {
            var value = "abcd";
            var subString = "abc";

            StringAssert.Contains(value, subString);
        }

        /// <summary>
        /// 指定した文字列で始まるかをテスト（プレフィックスの確認）
        /// </summary>
        [TestMethod]
        public void StartWith_value()
        {
            var value = "abcd";
            var subString = "abc";

            StringAssert.StartsWith(value, subString);
        }

        /// <summary>
        /// 指定した文字列で終わるかをテスト（サフィックスの確認）
        /// </summary>
        [TestMethod]
        public void EndsWith_value()
        {
            var value = "abcd";
            var subString = "cd";

            StringAssert.EndsWith(value, subString);
        }
    }
}
```

## Reference
- [Visual Studio のテスト ツール](https://learn.microsoft.com/ja-jp/visualstudio/test/?view=vs-2022)
- [MSTest Documentation](https://github.com/microsoft/testfx/blob/main/docs/README.md)
- [.NET Core と .NET Standard での単体テストのベスト プラクティス](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-best-practices)
- [testfx](https://github.com/microsoft/testfx)
- [xUnit.net](https://github.com/xunit/xunit)
- [nunit](https://github.com/nunit/nunit)