[.NET][C#]NUnitのAttribute_テストメソッド、テストクラス系を試す

# はじめに
今回はNUnitのAttributeで最も重要な
テストメソッド、テストクラスに付けるAttributeを調べてみた。

▼NUnit関連記事
[[.NET][C#]NUnitを使用して単体テスト自動化　基本編](https://qiita.com/suganury/items/d255ae140373af7d0146)
[[.NET][C#]NUnitを使用した単体テストのカバレッジレポートを自動生成する](https://qiita.com/suganury/items/62e0770d16eb579799d8)
[[.NET][C#]NUnitのClassic Modelアサーションメソッド一覧](https://qiita.com/suganury/items/9f2c3af979ca071726f2) ※Assertクラス
[[.NET][C#]NUnitのStringAssertアサーションメソッド一覧](https://qiita.com/suganury/items/e6d9e87ba7f23bfb15a7)
[[.NET][C#]NUnitのCollectionAssertアサーションメソッド一覧](https://qiita.com/suganury/items/442dccf2b99f790a9354)
[[.NET][C#]NUnitのFileAssert・DirectoryAssertアサーションメソッド一覧](https://qiita.com/suganury/items/1e2cf02a08c759bb4c63)

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

# NUnit Attribute
ソースコードがテスト用のものであることを実行環境に伝えるマーキングで
テスト用のクラスやメソッドに付ける。
また、テスト用のパラメータの指定や事前・事後処理などの実行順序の定義するものなど、様々なAttributeがある。

今回はNUnitを実行するために必須である
テストクラス、テストメソッド系のAttributeを試す。

## 1. TestFixture
テストクラスであることを示すAttribute。

```C#:パラメータ無し
[TestFixture]
public class Tests {
    [Test]
    public void Test1()
    {
        Assert.Pass();
    }
}
```
上記のようにクラスに付ける。
クラスに`[TestFixture]`が付いているだけではテストが実行されるわけではなく、
`[Test]`や`[TestCase]`といったAttributeが付いたテストメソッドを持たないといけない。
逆にテストメソッドがあるのであれば`[TestFixture]`は無くてもいい。


```C#:型を指定
[TestFixture(typeof(string))]
public class Tests<T>
{
    [Test]
    public void TypeTest()
    {
        object obj = DummyTarget.getAnonymousObject();
        Assert.IsInstanceOf<T>(obj);
    }
}
```
上記のようにテストコード中の型を固定したくない場合は、
テストクラスにジェネリクスを付け、`[TestFixture]`の引数にType型を指定する。

```C#:パラメータ指定
[TestFixture("MBr", "マリオ", "ピーチ", "クッパ")]
[TestFixture("LoZ", "リンク", "ゼルダ", "ガノン")]
[TestFixture("Pok", "サトシ", "カスミ", "シゲル")]
public class ParamTestFixture
{
    readonly NintendoGameCharacter NCharacter;
    readonly string ExpectHero;
    readonly string ExpectHeroine;
    readonly string ExpectRival;

    public ParamTestFixture(string title, string hero, string heroine, string rival) {
        this.NCharacter = DummyTarget.getNintendoGameCharacter(title);
        this.ExpectHero = hero;
        this.ExpectHeroine = heroine;
        this.ExpectRival = rival;
    }
    
    [Test]
    public void NintendoHeroTest()
    {
        Assert.AreEqual(this.ExpectHero, NCharacter.Hero);
    }
    
    [Test]
    public void NintendoHeroineTest()
    {
        Assert.AreEqual(this.ExpectHeroine, NCharacter.Heroine);
    }
    
    [Test]
    public void NintendoRivalTest()
    {
        Assert.AreEqual(this.ExpectRival, NCharacter.Rival);
    }
}
```
上記のようにテストクラスのコンストラクタの引数を指定することもできる。
引数の型と数は、テストクラスのコンストラクタの引数と一致させる必要がある。
上記の場合、テスト実行時に各パラメータがコンストラクタに指定されたParamTestFixtureオブジェクトが3つ生成され、それぞれのテストメソッドが実行される。

テストクラス内の各メソッドで一貫したテストパラメータを使用したい場合は使うと良さそう。


## 2. TestFixtureSource
テストクラスに付けるAttribute。
変数等の要素をテストのパラメータとして指定することが可能。

```C#:変数をパラメータとして指定
[TestFixtureSource("ParamSource")]
public class ParamTestFixtureSource
{
    static object[] ParamSource =
    {
        new string[] {"MBr", "マリオ", "ピーチ", "クッパ"},
        new string[] {"LoZ", "リンク", "ゼルダ", "ガノン"},
        new string[] {"Pok", "サトシ", "カスミ", "シゲル"},
    };

    readonly NintendoGameCharacter NCharacter;
    readonly string ExpectHero;
    readonly string ExpectHeroine;
    readonly string ExpectRival;

    public ParamTestFixtureSource(string title, string hero, string heroine, string rival) {
        this.NCharacter = DummyTarget.getNintendoGameCharacter(title);
        this.ExpectHero = hero;
        this.ExpectHeroine = heroine;
        this.ExpectRival = rival;
    }
    
    [Test]
    public void NintendoHeroTest()
    {
        Assert.AreEqual(this.ExpectHero, NCharacter.Hero);
    }
    
    [Test]
    public void NintendoHeroineTest()
    {
        Assert.AreEqual(this.ExpectHeroine, NCharacter.Heroine);
    }
    
    [Test]
    public void NintendoRivalTest()
    {
        Assert.AreEqual(this.ExpectRival, NCharacter.Rival);
    }
}


```
クラス内のフィールド、プロパティ、メソッドをテスト用のパラメータとして指定できる。
フィールドの場合、上記のように変数名を指定する。
指定するパラメータはいずれも**staticである必要がある**。

上記例ではパラメータに配列を使用しているが、コレクション（IEnumerable実装クラス）でも可。
`[TestFixture]`でパラメータを指定するのと同様、
コンストラクタのメソッドシグニチャとパラメータの位置や型を一致させる必要がある。


```C#:外部クラスの変数をパラメータとして指定
class ParamWarehouse {
    static object[] ParamSource =
    {
        new string[] {"MBr", "マリオ", "ピーチ", "クッパ"},
        new string[] {"LoZ", "リンク", "ゼルダ", "ガノン"},
        new string[] {"Pok", "サトシ", "カスミ", "シゲル"},
    };
}

[TestFixtureSource(typeof(ParamWarehouse), "ParamSource")]
public class ParamTestFixtureSource2
{
    readonly NintendoGameCharacter NCharacter;
    readonly string ExpectHero;
    readonly string ExpectHeroine;
    readonly string ExpectRival;

    public ParamTestFixtureSource2(string title, string hero, string heroine, string rival) {
        this.NCharacter = DummyTarget.getNintendoGameCharacter(title);
        this.ExpectHero = hero;
        this.ExpectHeroine = heroine;
        this.ExpectRival = rival;
    }
    
    [Test]
    public void NintendoHeroTest()
    {
        Assert.AreEqual(this.ExpectHero, NCharacter.Hero);
    }
    
    [Test]
    public void NintendoHeroineTest()
    {
        Assert.AreEqual(this.ExpectHeroine, NCharacter.Heroine);
    }
    
    [Test]
    public void NintendoRivalTest()
    {
        Assert.AreEqual(this.ExpectRival, NCharacter.Rival);
    }
}
```
上記のように別クラスの変数、プロパティ、メソッドを指定することも可能。
`[TestFixtureSource]`の第一引数にパラメータを持つクラスのType型、第二引数に変数名などを指定。

使用できるパラメータの条件などは、パラメータのみ指定する場合と同じ。


```C#:メソッドでパラメータをロジカルに生成
[TestFixtureSource(nameof(ParamWarehouseMethod))]
public class ParamTestFixtureSource3
{
    readonly NintendoGameCharacter NCharacter;
    readonly string ExpectHero;
    readonly string ExpectHeroine;
    readonly string ExpectRival;

    public ParamTestFixtureSource3(string title, string hero, string heroine, string rival) {
        this.NCharacter = DummyTarget.getNintendoGameCharacter(title);
        this.ExpectHero = hero;
        this.ExpectHeroine = heroine;
        this.ExpectRival = rival;
    }
    
    [Test]
    public void NintendoHeroTest()
    {
        Assert.AreEqual(this.ExpectHero, NCharacter.Hero);
    }
    
    static object[] ParamGenerateMethod() {
        return new object[]
        {
            new string[] {"MBr", "マリオ", "ピーチ", "クッパ"},
            new string[] {"LoZ", "リンク", "ゼルダ", "ガノン"},
            new string[] {"Pok", "サトシ", "カスミ", "シゲル"},
        };
    }
}
```
上記のようにパラメータを生成するメソッドを指定することもできる。
上記例では単純にパラメータの配列を返しているだけだが、
ロジカルにパラメータを増幅、生成することも可能だ。

## 3. Test
テストメソッドであることを示すAttribute。

```C#:[Test]
public class TestTest {

    [Test]
    public void MarioBrosHeroTest()
    {
        string TargetGame = "MBr";
        string ExpectHero = "マリオ";

        Assert.AreEqual(ExpectHero, DummyTarget.getNintendoGameCharacter(TargetGame).Hero);
    }

    [Test(Description = "ポケモン主人公の妥当性をテストします")]
    public void PokemonHeroTest()
    {
        string TargetGame = "Pok";
        string ExpectHero = "サトシ";

        Assert.AreEqual(ExpectHero, DummyTarget.getNintendoGameCharacter(TargetGame).Hero);
    }
    
    [Test(ExpectedResult = "リンク")]
    public string ZeldaHeroTest()
    {
        string TargetGame = "LoZ";

        return DummyTarget.getNintendoGameCharacter(TargetGame).Hero;
    }
}
```
このAttributeを付けたメソッドがテストメソッドとして実行される。
下記のAttributeパラメータを指定できる。

`Description`：テストの説明
`ExpectedResult`：テスト想定結果

`ExpectedResult`を指定する場合は、メソッド中Assertクラスによるアサーションはせず、テスト対象の実行結果を戻り値として返すようにする。

簡単なテストなら上記で良いが、
後記の`[TestCase]`では上記以上のことができるため、
`[Test]`は使わず`[TestCase]`で統一していいかもしれない。


## 4. TestCase
テストメソッドであることを示すAttribute。
`[Test]`よりも多機能。

```C#:[TestCase]
public class TestCaseTest {

    [TestCase]
    public void MarioBrosHeroTest()
    {
        string TargetGame = "MBr";
        string ExpectHero = "マリオ";

        Assert.AreEqual(ExpectHero, DummyTarget.getNintendoGameCharacter(TargetGame).Hero);
    }
    
    [TestCase("MBr", 1983, "マリオ")]
    [TestCase("LoZ", 1986, "リンク")]
    [TestCase("Pok", 1996, "サトシ")]
    public void NintendoHeroTest(string targetGame, int expectFirst, string expectHero)
    {
        NintendoGameCharacter NCharacter = DummyTarget.getNintendoGameCharacter(targetGame);

        Assert.AreEqual(expectFirst, NCharacter.FirstAppearance);
        Assert.AreEqual(expectHero, NCharacter.Hero);
    }
    
    [TestCase("MBr", ExpectedResult="ピーチ")]
    [TestCase("LoZ", ExpectedResult="ゼルダ")]
    [TestCase("Pok", ExpectedResult="カスミ")]
    public string NintendoHeroineTest(string targetGame)
    {
        return DummyTarget.getNintendoGameCharacter(targetGame).Heroine;
    }
}
```
`[TestCase]`のみの指定であれば`[Test]`のみ指定するのと同じ。
上から2番目の例のようにテストメソッドに引数を指定することが可能。
また、3番目のように引数と想定結果を指定することも可能。

```C#:[TestCase]の様々な名前付きパラメータ
    [TestCase(
    //▼テストケースの作者
    Author="suganury"
    //▼カテゴリ. dotnet test --filter TestCategory={CategoryName}のオプションで特定のカテゴリのみテスト実行可能
    , Category="hogeCategory"
    //▼テストケースの説明
    , Description="説明だYO"
    //▼テストを実行しないプラットフォームをカンマ区切りで指定。OSや.NETバージョンなどを指定可能
    , ExcludePlatform="Net-1.0,Windows8"
    //▼想定結果。Assertクラス等によるアサーションではなく、戻り値に実行結果を返すようにする。
    , ExpectedResult="hoge"
    //▼テスト無効フラグ。trueにするとテストが実行されなくなる
    , Explicit=false
    //▼テスト無効理由。Explicitとセットで使う
    , Reason="hogeしか返ってこんし"
    //▼テスト無視フラグと理由。このAttributeが付いているとテストが実行されなくなる
    , Ignore="hogeしか返ってこんし"
    //▼Ignoreと全く同じ。どちらか1つでいい
    , IgnoreReason="hogeしか返ってこんし"
    //▼テスト名。デフォルトではメソッド名になる
    , TestName="Hogeテスト"
    //▼対象のテストクラス
    , TestOf=typeof(String)
    )]
    public string test(){
        return "hoge";
    }
```
`ExpectedResult`の他にも様々なAttributeパラメータが指定できる。
あまり指定しすぎるとゴチャゴチャするので、
`[Category]`などはそれ用のAttributeを指定するのがいいかもしれない。


## 5. TestCaseSource
テストメソッドであることを示すAttribute。
変数等の要素をテストのパラメータとして指定することが可能。
`[TestFixtureSource]`のテストメソッド版。

```C#:クラス内の変数をパラメータとして指定
public class TestCaseSourceTest {

    static object[] NintendoCharaCases =
    {
        new object[] { "MBr", 1983, "マリオ" },
        new object[] { "LoZ", 1986, "リンク" },
        new object[] { "Pok", 1996, "サトシ" }
    };

    static IList<object> NintendoCharaCaseList = new List<object>
    {
        new object[] { "MBr", 1983, "マリオ" },
        new object[] { "LoZ", 1986, "リンク" },
        new object[] { "Pok", 1996, "サトシ" }
    };
    
    [TestCaseSource("NintendoCharaCases")]
    [TestCaseSource("NintendoCharaCaseList")]
    public void NintendoCharaTest(string targetGame, int expectFirst, string expectHero)
    {
        NintendoGameCharacter NCharacter = DummyTarget.getNintendoGameCharacter(targetGame);

        Assert.AreEqual(expectFirst, NCharacter.FirstAppearance);
        Assert.AreEqual(expectHero, NCharacter.Hero);
    }
}
```
上記例のように自クラスの配列もしくはコレクションをテストメソッドのパラメータとして指定できる。
パラメータとして指定するものは**staticである必要がある**。
使用時の制約としては、`[TestFixtureSource]`と概ね同じ。

```C#:外部のクラスの変数をパラメータとして指定
public class TestCaseSourceOtherClass {

    [TestCaseSource(typeof(TestCaseSourceTest), "NintendoCharaCases")]
    public void NintendoCharaTestOtherClass(string targetGame, int expectFirst, string expectHero)
    {
        NintendoGameCharacter NCharacter = DummyTarget.getNintendoGameCharacter(targetGame);

        Assert.AreEqual(expectFirst, NCharacter.FirstAppearance);
        Assert.AreEqual(expectHero, NCharacter.Hero);
    }
}
```
上記のように外部のクラスが持つstaticフィールド等をパラメータとして指定することも可能。

```C#:メソッドでパラメータをロジカルに生成
    [TestCaseSource(nameof(GenerateTestCharacter), new object[] { "Hero" })]
    public void NintendoHeroTest(string expectHero)
    {
        List<string> nintendoHeros = DummyTarget.nintendoChara
                                                .Values
                                                .Select(n => n.Hero)
                                                .ToList();
        
        CollectionAssert.Contains(nintendoHeros, expectHero);
    }

    static IList<string> GenerateTestCharacter(string charaType)
    {
        IList<string> list = new List<string>();
        if(charaType.Equals("Hero")) {
            list.Add("マリオ");
            list.Add("リンク");
            list.Add("サトシ");
            list.Add("ロックマン");
            list.Add("クラウド");
        } else if(charaType.Equals("Heroine")) {
            list.Add("ピーチ");
            list.Add("ゼルダ");
            list.Add("カスミ");
            list.Add("ロール");
            list.Add("ティファ");
        } else if(charaType.Equals("Rival")) {
            list.Add("クッパ");
            list.Add("ガノン");
            list.Add("シゲル");
            list.Add("フォルテ");
            list.Add("セフィロス");
        }
        return list;
    }
```
また、上記のように`[TestCaseSource]`の第一引数に配列・コレクションを返すメソッド、第二引数にそのメソッドの引数を指定することで、
パラメータをロジカルに生成することが可能。
上記のテスト実行結果は下記となる。

```:実行結果:
NUnit Adapter 3.15.1.0: Test execution complete
  √ NintendoHeroTest("マリオ") [5ms]
  √ NintendoHeroTest("リンク") [1ms]
  √ NintendoHeroTest("サトシ") [< 1ms]
  X NintendoHeroTest("ロックマン") [27ms]
  エラー メッセージ:
     Expected: some item equal to "ロックマン"
  But was:  < "マリオ", "リンク", "サトシ" >

  X NintendoHeroTest("クラウド") [< 1ms]
  エラー メッセージ:
     Expected: some item equal to "クラウド"
  But was:  < "マリオ", "リンク", "サトシ" >
```
場合によっては様々なパターンのパラメータをテストすることもあるだろうし、それをテストコードにハードコードするのは苦しい場面もあるだろう。
そういったときにこれは便利な機能だと思う。


# まとめ
今回記載したAttributeとアサーションメソッドを知っておけば
最低限のテスト実施自体はできると思う。
が、やはりテストのパフォーマンス等を求めるとなると事前・事後処理用のAttributeは欲しいし、
カテゴライズしたり無効にしたりするAttributeも使う機会はあるだろう。

そこらへんはまた今度:tea:

# 参考
[NUnit公式：Attribute一覧ページ](https://docs.nunit.org/articles/nunit/writing-tests/attributes.html)