xUnit.net でユニットテストを始める

xUnit.netを使ってテストする方法として、テストプロジェクトの作成、テストの作成、テストの整理 のやり方を紹介。　※この記事は Visual Studio 2017 / xUnit.net 2.2 の環境で書かれています。

# とりえあずテスト

## テストプロジェクトの作成
※注意：テスト対象のプロジェクト(Desktop, .NET Core, UWP, ...)によって違うので [About xUnit.net - Getting Started](https://xunit.github.io/#documentation) を見てね。

クラシック(!)デスクトップアプリのテストプロジェクトにはクラスライブラリプロジェクトを使う。
![1.png](https://qiita-image-store.s3.amazonaws.com/0/118451/5c434f42-b04a-0909-3078-f5f1957cdd04.png)


### xUnit.net のインストール
NuGet パッケージマネージャーコンソールで xunit をインストールする。インストールするプロジェクトを間違えないように。

※NuGet パッケージマネージャーコンソールがない場合は メニュー ⇒ ツール⇒ NuGet パッケージマネージャー ⇒ パッケージマネージャコンソール で表示できる。

```
PM> Install-Package xunit
PM> Install-Package xunit.runner.visualstudio
```
![2.png](https://qiita-image-store.s3.amazonaws.com/0/118451/6c973dae-fa29-c04a-90d7-ea3102b0a895.png)

### コードスニペット

コードスニペットがあると捗るので入れておく。[xUnitTestCodeSnippets の xunit-testmethod.snippet](https://github.com/jsakamoto/xUnitTestCodeSnippets/blob/master/csharp/xUnit/xunit-testmethod.snippet) を適当な場所にダウンロードし、Visual Studio に インポートする。

手順：メニュー ⇒ ツール ⇒ コードスニペットマネージャー ⇒ インポート ⇒ xunit-testmethod.snippet

インポート後、コードスニペット xtestm, fact, afact, theory, atheory, xtestc が使えるようになる。
![0.png](https://qiita-image-store.s3.amazonaws.com/0/118451/740d236a-eaa8-ebbf-49ba-c33a4028b3cf.png)


### Chainning Assertion

[Chainning Assertion](https://github.com/neuecc/ChainingAssertion) は @neuecc 氏が作成したテストコードが書きやすくなる拡張メソッドと便利メソッド集。xUnit.net に必須ではないが、イケてるので入れておく。この記事でも Chainning Assertion を使う。

通常の Assert と Chaining Assertion の比較：下のほうが好きになれそうな人にはおススメ。

```csharp
// xUnit.net: Assert
Assert.Equal("test1", test);

// Chaining Assertion: Is
test.Is("test1");
```

Chaining Assertion は NuGet にもあるけど古いので、githubから直接コードをコピってテストプロジェクトにぶち込むのが良い。
[https://github.com/neuecc/ChainingAssertion/blob/master/ChainingAssertion.xUnit/ChainingAssertion.xUnit.cs](https://github.com/neuecc/ChainingAssertion/blob/master/ChainingAssertion.xUnit/ChainingAssertion.xUnit.cs)

この ChainingAssertion.xUnit.cs にはチュートリアルも乗ってるので、使ったことない人は眺めておくとよい。


![3.png](https://qiita-image-store.s3.amazonaws.com/0/118451/4eb88016-5dee-0b8d-e381-68279cc71364.png)


## テストの作成

こんなかんじ。

```csharp
 public class Class1
 {
     [Fact]
     public void Test()
     {
         (1 + 1).Is(2);
     }
 }
```

## テストの実行
ビルドするとテストエクスプローラーに作成したテストが表示されるので、選択して実行すればよい。

※テストエクスプローラーがない場合は メニュー ⇒ テスト ⇒ ウィンドウ ⇒ テストエクスプローラー で表示できる。

![4.png](https://qiita-image-store.s3.amazonaws.com/0/118451/923783ff-0b09-bde0-f5c8-b53968350aaa.png)


# テストの作り方いろいろ
メソッドに Fact または Theory 属性のどちらかを付けるとテストケースとして認識されるようになる。

## Fact
通常のテスト。

```csharp
// テスト対象メソッド
public int Add(int x, int y)
{
    return x + y;
}

// テストメソッド
[Fact]
public void Test()
{
    Add(1, 1).Is(2);
}
```

### DisplayName
テストエクスプローラーに表示されるテスト名を DisplayName で設定できる。DisplayName を設定しない場合はメソッド名が表示される。

```csharp
[Fact(DisplayName = "1+1=2のはず")]
public void Test()
{
    (1 + 1).Is(2);
}
```

![5.png](https://qiita-image-store.s3.amazonaws.com/0/118451/0168cd99-e502-fccb-e62d-4ada19ce61fc.png)


### Skip
テストを一時的に無効化したい場合は Skip を設定する。

```csharp
// テストメソッド
[Fact(DisplayName = "1+1=2のはず", Skip = "諸々の事情により実行したくない")]
public void Test()
{
    Add(1, 1).Is(2);
}
```


## Theory
パラメータ付きテストを作成したいときは Fact の代わりに Theory を使う。
Fact と同様に DisplayName と Skip は設定できる。

### InlineData
入力値が数値や文字列ならこれで。複数並べることもできる。

```csharp
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
public void AddTest(int x, int y, int ans)
{
    Add(x, y).Is(ans);
}
```

### MemberData
入力値をプログラムで生成したい場合は MemberData が便利。また、入力値が複雑な型の場合は InlineData を使えないので MemberData を使う。

戻り値が IEnumerable&lt;object[]&gt; なプロパティまたはメソッドをテストデータとして使用できる。

```csharp
// テストデータ１: プロパティ
public static object[][] AddTestData => Enumerable.Range(1, 10).Select(i => new object[] { i, i, i + i }).ToArray();

// テストデータ２: メソッド
public static IEnumerable<object[]> MakeAddTestData(int from, int count)
{
    return Enumerable.Range(from, count).Select(i => new object[] { i, i, i + i });
}

// テストメソッド
[Theory]
[MemberData(nameof(AddTestData))]
[MemberData(nameof(MakeAddTestData), 2, 100)]
[MemberData(nameof(MakeAddTestData), 12, 100)]
public void AddTest2(int x, int y, int ans)
{
    Add(x, y).Is(ans);
}
```

### ClassData
テストデータ作成方法が複雑な場合、上記のMemberDataを使うとテストコードが見づらくなってくる。そんなときはテストデータ作成ロジックを別のクラスに持たせることもできる。

```csharp
// テスト対象メソッド
public string GetYoubi(DateTime date)
{
    return date.ToString("dddd", new System.Globalization.CultureInfo("ja-JP"));
}

// テストデータ作成クラス
class TestDataClass : IEnumerable<object[]>
{
    List<object[]> _testData = new List<object[]>();

    public TestDataClass()
    {
        _testData.Add(new object[] { new DateTime(2017, 4, 27), "木曜日" });
        _testData.Add(new object[] { new DateTime(2017, 4, 28), "金曜日" });
    }

    public IEnumerator<object[]> GetEnumerator() => _testData.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// テストメソッド
[Theory]
[ClassData(typeof(TestDataClass))]
public void TestDayOfWeek(DateTime date, string youbi)
{
    GetYoubi(date).Is(youbi);
}
```

### ファイルからテストデータを読みたい場合
DataAttribute を継承したカスタム属性を作成すればよい。[ExcelDataExample](https://github.com/xunit/samples.xunit/tree/master/ExcelDataExample) が参考になる。



## Setup と Teardown

xUnit.net ではテストの Setup はコンストラクタ、Teardown は IDisposable.Dispose() に定義する。

```csharp
public class Class3 : IDisposable
{
    public Class3()
    {
        // test setup
    }

    public void Dispose()
    {
        // teardown
    }

    [Fact]
    public void Test()
    {
        "1".Is("1");
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    public void Test2(int i)
    {
        "2".Is("2");
    }
}
```

ちなみにインスタンスはテスト実行毎に作成される。上記の例では、Class3 は3回インスタンス化される。

## Exception のテスト

Assert.Throws() か Assert.ThrowsAny() を使う。

```csharp
// テスト対象メソッド
public void Write(string message)
{
    if(message == null)
    {
        throw new ArgumentNullException(nameof(message), "ぬるぬる");
    }
    Console.WriteLine(message);
}

// テストメソッド
[Fact]
public void Test()
{
    // ArgumentNullExceptionが発生するはず
    var ex = Assert.Throws<ArgumentNullException>(() =>
    {
        Write(null);
    });
    ex.Message.Contains("ぬるぬる").IsTrue();

    // ArgumentExceptionかそのサブクラスの例外が発生するはず
    var ex2 = Assert.ThrowsAny<ArgumentException>(() =>
    {
        Write(null);
    });
}
```

## 非同期メソッドのテスト

テストメソッドを戻り値 Task で定義する。Exception のテストには、Assert.ThrowsAsync() や Assert.ThrowsAnyAsync() が用意されている。 

```csharp
// テスト対象メソッド
public async Task<string> ReadAllTextAsync(string file)
{
    if (file == null)
    {
        throw new ArgumentNullException(nameof(file));
    }

    using(var sr = new StreamReader(file))
    {
        return await sr.ReadToEndAsync();
    }
}

// テストメソッド
[Fact]
public async Task ReadAllTextReturnsFileContent()
{
    var str = await ReadAllTextAsync("test");
    str.Is("hogehoge");
}

// テストメソッド(Exception)
[Fact]
public async Task ReadAllTextAsyncThrowsArgumentNullException()
{
    // ArgumentNullExceptionが発生するはず
    var ex = await Assert.ThrowsAsync<ArgumentNullException>(() =>
    {
        return ReadAllTextAsync(null);
    });
}
```

## デバッグメッセージの出力
xUnit.net では テストメソッドの中で Debug.WriteLine(); してもメッセージ出力を確認できない。代わりに ITestOutputHelper インタフェースを使う。

```csharp
public class Class6
{
    private readonly ITestOutputHelper output;

    public Class6(ITestOutputHelper output)
    {
        this.output = output;
    }

    [Fact]
    public void Test()
    {
        output.WriteLine("test message");
    }
}
```

![7.png](https://qiita-image-store.s3.amazonaws.com/0/118451/167b2be1-8a30-bd7b-592d-d7ec081b45db.png)



## Trait
タグ付け的なことをしたかったら Trait.

```csharp
[Fact(DisplayName = "1+1=2のはず")]
[Trait("Category", "Arithmetic")]
[Trait("Priority", "1")]
public void Test()
{
    Add(1, 1).Is(2);
}

[Theory(DisplayName = "足し算")]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
[Trait("Category", "Arithmetic")]
[Trait("Priority", "2")]
public void AddTest(int x, int y, int ans)
{
    Add(x, y).Is(ans);
}
```


テストエクスプローラーで右クリック ⇒ グループ化 ⇒ 特徴 で、Trait で分類される。

![6.png](https://qiita-image-store.s3.amazonaws.com/0/118451/30f88624-ba53-6511-a9bf-b3db15238144.png)



## その他メモ
テストプロジェクトのプラットフォームを x64 にするとテストエクスプローラーにテストが表示されなくなるので注意。

# まとめ
ここで紹介したやり方でだいたいのテストは作成できるようになる。はず。

テストを並列に実行したいとか、テストにコンテキストを持たせたいとかなってきたらドキュメントを参照。
[xUnit.net - Documentation](https://xunit.github.io/#documentation)

// テストが複雑になってきたと感じたら、もしかしたらそのテストは捨てたほうが良いかも。