C#　MSTestの基本的な使用方法についてまとめる

C#のユニットテスト用のフレームワークである、**MSTest**について使用方法の基本をまとめます。また、モックを用いたテストやカバレッジレポートの出力についてもまとめます。

# MSTestとは
MSTestは、Microsoftが提供するユニットテスト用のフレームワークで、Visual Studioに標準搭載されています。「.NET Framework、.NET Core、.NET、UWP、WinUI」など、様々な.NETターゲットで動作可能です。
**MSTest**のほかに、代表的なフレームワークとして**NUnit**や**xUnit**があります。

[MSTest公式ドキュメントはこちら](https://learn.microsoft.com/ja-jp/dotnet/core/testing/)

# テストプロジェクトの作成方法
1. ソリューションエクスプローラーでソリューションを右クリックし、```[追加] > [新しいプロジェクト]``` をクリック。
![テストプロジェクト作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/2ea0a351-39e7-bdd9-b103-5b5929ca6c89.png)

1. ```[MSTest プロジェクト]``` を選択し、テストプロジェクト名を入力。テストプロジェクトが作成される。
1. テストプロジェクトからテスト対象プロジェクトを参照するため、```テストプロジェクトの[依存関係]を右クリック > [プロジェクト参照の追加] をクリック``` し、テスト対象プロジェクトを参照する。

すると、下記画像のようなコードが自動生成されテストを行える環境となります。

![テスト準備完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/91316757-33b6-a1ce-f93e-100ad6ab3918.png)

# MSTestにおける属性と基本ルール
特定のクラスがテストクラスであることを示すため、TestClass属性を指定する必要があります。同様にテストメソッドには、TestMethod 属性を指定する必要があります。
属性を指定し、そのクラスやメソッドがテストクラス（メソッド）であること示します。
また、テストメソッドは```public void```もしくは```public Task```として定義する必要があります。
```MSTest.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class TestClass
{
    [TestMethod]
    public void TestMethod()
    {
        // テスト処理
    }
}
```

# MSTestにおけるアサーション（Assert）
MSTestでは、アサーション（Assert）を使用し、テスト結果が期待値と一致するかを検証・テストします。
```Microsoft.VisualStudio.TestTools.UnitTesting名前空間```の、```Assertクラス```を使用します。Assertクラスには様々なAssertメソッドが用意されており、これらを用いテストが成功したか失敗したかを判別します。

# MSTestにおける主なアサーションメソッド
**・Assert.AreEqual(expected, actual)/Assert.AreNotEqual(expected, actual)**
第一引数が期待値、第二引数が実際値。
期待される値（expected）と実際の値（actual）が等しいこと（等しくないこと）を確認。
引数には、基本データ型（int, double, string, bool）や参照型（オブジェクト）を指定可能。

**・Assert.IsTrue(boolean)/Assert.IsFalse(boolean)**
条件が真/偽であることを確認。

**・Assert.IsNull(object)/Assert.IsNotNull(object)**
指定したオブジェクトがnullであること/nullでないことを確認。

**・Assert.ThrowsException\<T>(Method method)**
指定したメソッド（アクション）が特定の例外をスローすることを確認。
引数\<T>には、期待される例外の型を指定。
引数(Method method)には、実行したいメソッドを指定。このメソッド内で、Tの型の例外が発生することを期待し、テストする。

```AssertSample.cs
 [TestMethod]
 [Ignore("pass")]
 public void TestAreEqual()
 {
     //期待値
     int expected = 1;
     //実際値
     int actual = 2;
     //Assert.AreEqual(expected, actual); 
     Assert.AreNotEqual(expected, actual);
 }

 [TestMethod]
 public void TestIsTrue()
 {
     bool result = false;
     //Assert.IsTrue(result);
     Assert.IsFalse(result);
 }

 [TestMethod]
 public void TestIsNull()
 {
     int? number = null;
     Assert.IsNull(number);
     //Assert.IsNotNull(number);
 }

 [TestMethod]
 public void TestThrowsException()
 {
     Assert.ThrowsException<ArgumentNullException>(() => { throw new ArgumentNullException(); });
 }
```

[Assert Class公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.testtools.unittesting.assert?view=visualstudiosdk-2022
)
[Assert.ThrowsException公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.testtools.unittesting.assert.throwsexception?view=visualstudiosdk-2019
)

# AreEqualの注意点
・Assert.AreEqualは参照型オブジェクトでは参照比較を行う為、異なるインスタンスであれば内容が同じでも下記のようなテスト(TestFailureAreEqual)は失敗します。(参照先が同じなら成功)
・リストや配列などの参照型の比較を行いたい場合は、「CollectionAssert Class」を使用してください。
```CollectionAssertSample.cs
[TestMethod]
public void TestFailureAreEqual()
{
    //期待値
    List<string> expected = new List<string> { "aaa", "bbb", "ccc" };
    //実際値
    List<string> actual = new List<string> { "aaa", "bbb", "ccc" };
    Assert.AreEqual(expected, actual);
}

[TestMethod]
public void TestCollectionAreEqual()
{
    //期待値
    List<string> expected = new List<string> { "aaa", "bbb", "ccc" };
    //実際値
    List<string> actual = new List<string> { "aaa", "bbb", "ccc" };
    CollectionAssert.AreEqual(expected, actual);
    //CollectionAssert.AreNotEqual(expected, actual);
}
```
[CollectionAssert公式ドキュメント](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.testtools.unittesting.collectionassert?view=visualstudiosdk-2022)

# MSTestの実行
テスト対象として簡単なUserクラス(User.cs)とそのテストクラス(UserTest.cs)を用意しました。

```User.cs
 public class User
 {
     private string _name;
     private string _password;

     public User() { }

     public User(string name, string password)
     {
         _name = name;
         _password = password;
     }

     
     public string Name
     {
         get { return _name; }
         set 
         {   
             if(string.IsNullOrEmpty(value))
             {
                 throw new ArgumentNullException();
             }
             _name = value; 
         }
     }

     
     public string Password
     {
         get { return _password; }
         set 
         {   
             if(string.IsNullOrEmpty(value))
             {
                 throw new ArgumentNullException();
             }
             _password = value; 
         }
     }
 }
```

```UserTest.cs
 [TestClass]
 public class UserTest
 {
     [TestMethod]
     public void ユーザー名とパスワードの設定が正しく行われることを確認する()
     {
         string expectedName = "John";
         string expectedPassword = "password";

         User user01 = new User();
         
         user01.Name = expectedName;
         user01.Password = expectedPassword;

         Assert.AreEqual(expectedName, user01.Name);
         Assert.AreEqual(expectedPassword, user01.Password);

     }

     [TestMethod]
     public void ユーザー名とパスワードに空文字を設定すると例外がスローされることを確認する()
     {

         User user01 = new User();
         Assert.ThrowsException<ArgumentNullException>(() => { user01.Name = ""; });
         Assert.ThrowsException<ArgumentNullException>(() => { user01.Password = ""; });
     }

     [TestMethod]
     public void コンストラクタでユーザー名とパスワードが正しく初期化されることを確認する()
     {
         string expectedName = "John";
         string expectedPassword = "password";

         User user = new User(expectedName, expectedPassword);

         Assert.AreEqual(expectedName, user.Name);
         Assert.AreEqual(expectedPassword, user.Password); 

     }
 }
```
UserTest.csのメソッド名は、テスト内容とわかりやすさ重視の為、日本語表記にしてあります。

# MSTestの機能
■ **初期化とクリーンアップを提供する属性**
 複数のテストに共通するセットアップとクリーンアップを独立したメソッドに抽出し、決められたタイミングで初期化とクリーンアップを行います。「アセンブリレベル・クラスレベル・テストレベル」の３つあります。
 ```Sample.cs
  [AssemblyInitialize]
 public static void AssemblyInitialize(TestContext testcontext)
 {
     //アセンブリの読み込みの直後に呼び出される
     //staticで引数に「TestContext」が必要
 }

 [AssemblyCleanup]
 public static void AssemblyCleanup()
 {
     //アセンブリのアンロードの直前に呼び出される
     //staticがある必要がある。引数は必要なし
 }

 [ClassInitialize]
 public static void ClassInitialize(TestContext context)
 {
     //クラスが読み込まれる直前 (ただし静的コンストラクターの後) に呼び出される
     //staticで引数に「TestContext」が必要
     //テストクラス全体で一度だけ呼び出される
 }

 [ClassCleanup]
 public static void ClassCleanup()
 {
     //クラスがアンロードされた直後に呼び出され
     //staticがある必要がある。引数は必要なし
 }

 [TestInitialize]
 public void TestInitialize()
 {
     //各テストメソッドが開始される直前に呼び出される
 }

 [TestCleanup]
 public void TestCleanup()
 {
     //各テストメソッドが終了した直後に呼び出される
 }
 ```
```UserTest.cs```でテストメソッドごとに記述していた、 Userオブジェクトの初期化を```[TestInitialize]```で行うと初期化を一か所にまとめることができます。

```UserTest.cs
[TestClass]
public class UserTest
{
    // メンバ変数として用意
    private User user01;

     [TestInitialize]
     public void TestInitialize()
     {
         //テストメソッド実行の度に１回呼び出される
         user01 = new User();
     }

     [TestMethod]
    public void ユーザー名とパスワードの設定が正しく行われることを確認する()
    {
        string expectedName = "John";
        string expectedPassword = "password";
    
        //User user01 = new User();　必要なくなる
        
        user01.Name = expectedName;
        user01.Password = expectedPassword;
    
        Assert.AreEqual(expectedName, user01.Name);
        Assert.AreEqual(expectedPassword, user01.Password);
    
    }
    
    [TestMethod]
    public void ユーザー名とパスワードに空文字を設定すると例外がスローされることを確認する()
    {
    
        //User user01 = new User();　必要なくなる
        Assert.ThrowsException<ArgumentNullException>(() => { user01.Name = ""; });
        Assert.ThrowsException<ArgumentNullException>(() => { user01.Password = ""; });
    }

}
```

■ **テストメソッドにテストデータを渡したい**
・**DataRow属性**
テストメソッドに対して固定データを指定し、複数のテストケースを行える。単純なテストデータを渡したい時に向いています。
```DataRowSample.cs
[TestMethod]
[DataRow("John","password")]
[DataRow("Bob", "pass")]
public void DataRow属性でテストデータをセットする(string expectedName, string expectedPassword)
{
    User user = new User(expectedName, expectedPassword);

    Assert.AreEqual(expectedName, user.Name);
    Assert.AreEqual(expectedPassword, user.Password);

}
```
・**DynamicData属性**
実行時にデータが作成されるため、動的なテストデータでテストを行えます。
```DynamicDataSample.cs
    public static IEnumerable<object[]> Data
    {
        get
        {
            return new[]
            {
                new object[] { "John", "password" },
                new object[] { "Bob", "pass" },
                new object[] { "Nancy", "aaaaa"}
            };
        }
    }
    
    [TestMethod]
    [DynamicData(nameof(Data))]
    public void DynamicData属性でテストデータをセットする(string expectedName, string expectedPassword)
    {
        User user = new User(expectedName, expectedPassword);

        Assert.AreEqual(expectedName, user.Name);
        Assert.AreEqual(expectedPassword, user.Password);

    }
}
```
```DynamicData 属性```が受け取るデータは、IEnumerable\<object[]> 型である必要があります。各 object[] は、テストメソッドの引数に対応する値を持つ配列です。

■ **一部テストメソッドを実行したくない**
[Ignore("このテストをpassする")]属性を使用する。引数に文字列を渡すことも可能。テストを行わない理由などを記述するとわかりやすいです。
```IgnoreSample.cs
[TestMethod]
[Ignore("まだ実装されていないためpass")]
public void ユーザーの住所の設定が正しく行われることを確認する()
{

}
```
# モックを使用したテストを実行したい
**■ モックとは**
単体テストを行う際に、テスト対象クラスが他クラスに依存している為、テストが難しくなることがあります。モックを用いることにより、依存先の振る舞いを模倣するオブジェクトを簡単に作ることができます。
C#でモックを使用したい時には、**Moq**ライブラリが使用されることが多いようです。

**■ Moqのインストール**
パッケージマネージャーコンソールから下記のコマンドを実行し、テストプロジェクトにインストールします。
```
Install-Package Moq
```
**■ Moqの実用例**
Moqを使用する場合は、そのモック対象とするオブジェクトのインターフェイスを用意することが推奨されているようです。
今回は、IDをもとにユーザー情報をDBから取得する機能をinterfaceとし、そのinterfaceに依存する```UserServiceクラス```をテスト対象とします。
```MoqSample.cs
public interface GetUserDB
{
    //idをもとにUserを取得する
    public User GetUserById(int id);
}

 public class UserService
 {
     private GetUserDB getUserDB;

     public UserService(GetUserDB getUserDB)
     {
         this.getUserDB = getUserDB;
     }

     public string GetUserName(int id)
     {
         //GetUserById(id)でidをもとにDB検索　今回のモック化の対象
         User user = getUserDB.GetUserById(id);
         if (user == null)
         {
             return "not found";
         }
         else 
         {
             return user.Name;
         }
     }
 }
```
```MoqTest.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

 [TestClass]
 public class UserTest
 {
    [TestMethod]
    public void Moqを用いたテスト１_指定したidでUserが見つかった時ユーザー名が返ってくるか確認する()
    {
        //Mockを作成
        var mock = new Mock<GetUserDB>();
        //テスト対象にMockオブジェクトを渡す
        UserService userService = new UserService(mock.Object);

        int userId = 1;
        string expectedName = "Bob";
        string expectedPassword = "password";
        User user = new User(expectedName, expectedPassword);

        //モックに、GetUserByIdメソッドを呼び出されたときにユーザーを返すよう設定
        mock.Setup(mock => mock.GetUserById(userId)).Returns(user);

        string result = userService.GetUserName(userId);

        Assert.AreEqual(expectedName, result);
    }

    [TestMethod]
    public void Moqを用いたテスト２_指定したidでUserが見つからなかった時メッセージが返ってくるか確認する()
    {
        //Mockを作成
        var mock = new Mock<GetUserDB>();
        //テスト対象にMockオブジェクトを渡す
        UserService userService = new UserService(mock.Object);

        int userId = 2;
        
        //モックにGetUserByIdメソッドを呼び出されたときにnullを返すよう設定
        //指定したIdでUserが見つからなかったことを想定
        mock.Setup(mock => mock.GetUserById(userId)).Returns((User)null);

        string result = userService.GetUserName(userId);

        Assert.AreEqual("not found", result);
    }
}
```

[moq(GitHub)](https://github.com/devlooped/moq/wiki/Quickstart)


https://qiita.com/usamik26/items/42959d8b95397d3a8ffb



# コードカバレッジを測定したい
**Coverlet**ライブラリを使用しカバレッジを測定することができます。測定結果はXML形式で出力されるので、**ReportGenerator**ライブラリを用いてさまざまな形式のレポートに変換します。今回はHTML形式で出力する方法をまとめます。

**1.CoverletとReportGeneratorのインストール**
パッケージマネージャで「Coverlet」を検索し、「coverlet.collector」をインストールします。また、下記のコマンドでReportGeneratorを、.NET グローバル ツールとしてインストールします。
```
dotnet tool install -g dotnet-reportgenerator-globaltool
```
**2.テストのコマンド実行**
visual studioの[表示] > [ターミナル]からshellを起動し、下記のコマンドでテストを実行する。```--results-directory:"./TestResults"```でカバレッジ測定結果の出力先を指定できます。
```
dotnet test --collect:"XPlat Code Coverage" --results-directory:"./TestResults"
```
完了後、指定ディレクトリに```coverage.cobertura.xml```が生成されます。

**3.ReportGeneratorで測定結果（XML形式）をHTML形式に変換**
```
reportgenerator -reports:"Path\To\TestProject\TestResults\{guid}\coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
```
* **-reports** : coverage.cobertura.xmlのパスを指定
* **-targetdir** : 生成するHTMLレポートの出力先を指定
* **-reporttypes** : レポートの出力形式（Html）を指定

完了後、-targetdirで指定したディレクトリに下記のようなレポートが生成されています。

全体のレポート
![カバレッジレポート1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/7d360a30-7a0b-8a1b-de41-cf768da04a08.png)

Classのレポート
![カバレッジレポート2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/7b5ba7b6-e0f9-16ba-7575-814aac1bc27a.png)

通過しなかったコードは視覚的にも確認できる
![カバレッジレポート3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3770474/79f7a262-5694-506b-27de-39672e36aae0.png)

[Microsoft公式ドキュメント](https://learn.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-code-coverage?tabs=windows)
[coverlet(GitHub)](https://github.com/coverlet-coverage/coverlet)
[ReportGenerator(GitHub)](https://github.com/danielpalme/ReportGenerator)

https://qiita.com/kojimadev/items/84d0a31a94fe6b22b450