Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

とりえあずテスト

テストプロジェクトの作成

※注意:テスト対象のプロジェクト(Desktop, .NET Core, UWP, ...)によって違うので About xUnit.net - Getting Started を見てね。

クラシック(!)デスクトップアプリのテストプロジェクトにはクラスライブラリプロジェクトを使う。
1.png

xUnit.net のインストール

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

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

PM> Install-Package xunit
PM> Install-Package xunit.runner.visualstudio

2.png

コードスニペット

コードスニペットがあると捗るので入れておく。xUnitTestCodeSnippets の xunit-testmethod.snippet を適当な場所にダウンロードし、Visual Studio に インポートする。

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

インポート後、コードスニペット xtestm, fact, afact, theory, atheory, xtestc が使えるようになる。
0.png

Chainning Assertion

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

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

// 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

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

3.png

テストの作成

こんなかんじ。

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

テストの実行

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

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

4.png

テストの作り方いろいろ

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

Fact

通常のテスト。

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

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

DisplayName

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

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

5.png

Skip

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

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

Theory

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

InlineData

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

[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<object[]> なプロパティまたはメソッドをテストデータとして使用できる。

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

// テストデータ2: メソッド
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を使うとテストコードが見づらくなってくる。そんなときはテストデータ作成ロジックを別のクラスに持たせることもできる。

// テスト対象メソッド
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 が参考になる。

Setup と Teardown

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

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() を使う。

// テスト対象メソッド
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() が用意されている。

// テスト対象メソッド
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 インタフェースを使う。

public class Class6
{
    private readonly ITestOutputHelper output;

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

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

7.png

Trait

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

[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

その他メモ

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

まとめ

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

テストを並列に実行したいとか、テストにコンテキストを持たせたいとかなってきたらドキュメントを参照。
xUnit.net - Documentation

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away