セージ の メモ書き

メモこそ命の恩人だ

C# - xUnit

xUnit

docs.microsoft.com

セットアップ

プロジェクトの追加

  • "追加 - 新しいプロジェクト" を選択。
  • "xUnit テスト プロジェクト" を選択。
  • "プロジェクト名.Tests" をプロジェクト名にする。

  • テストプロジェクトを作成完了。

  • 以下4つのパッケージがインストール済みの状態。

    • coverlet.collector
    • Microsoft.NET.Test.Sdk
    • xunit
    • xunit.runner.visualstudio

  • テストプロジェクトへの参照を追加。

テストクラスの追加

  • 名前空間は、テスト対象のプロジェクトに極力合わせる。
    • 管理しやすいので。
  • クラス名は、"テスト対象のクラス名 + Test" にする。
    • Sample クラスの場合、"SampleTest" クラス。


テストメソッド

以下の属性を付与すれば、テストメソッドとなる。

属性 テストメソッドの引数
Fact なし
Theory あり

Fact 属性

[Fact]
public void AddTest()
{
    var result = new SampleClass1().Add(1, 2);
    Assert.Equal(3, result);
}
  • Assert の結果でテストメソッドの成否が変わる。
  • エラーの場合、エラー箇所の情報が表示される。

Theory 属性

  • Theory の場合、テストメソッドに引数がないとエラーになる。
  • 入出力の一覧をまとめて適用できる。

InlineData 属性

[Theory]
[InlineData(1, 2, 3)]
[InlineData(1, 2, 999)]
public void AddTest(int in1, int in2, int expected)
{
    var result = new SampleClass1().Add(in1, in2);
    Assert.Equal(expected, result);
}
  • テストメソッドの上にパラメータが表示される。
    • 個人的には可読性が良い。

MemberData 属性

[Theory]
[MemberData(nameof(GetTestData))]
public void AddTest(int in1, int in2, int expected)
{
    var result = new SampleClass1().Add(in1, in2);
    Assert.Equal(expected, result);
}

private static IEnumerable<object[]> GetTestData()
{
    yield return new object[] { 1, 2, 3  };
    yield return new object[] { 1, 2, 999 };
}
  • static メソッドで定義する。
  • InlineData よりもロジックを定義できる。
  • .NET 6 で確認したところ、複数のテストケースとして表示される。
    • 以前は、単一のテストケースになっていたらしい。
    • テストケースを選択して実行しないと、"未実行" 扱いになる。

ClassData 属性

[Theory]
[ClassData(typeof(TestData))]
public void AddTest(int in1, int in2, int expected)
{
    var result = new SampleClass1().Add(in1, in2);
    Assert.Equal(expected, result);
}

private class TestData : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 1, 2, 3 };
        yield return new object[] { 1, 2, 999 };
    }
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
  • クラスでテストケースを定義する。
  • MemberData よりもロジックを定義できる。

テストメソッド(オプション)

DisplayName 属性

  • テストエクスプローラーでの名称を設定できる。
  • Fact・Theory の両方の属性で使用できる。
[Fact(DisplayName = "加算のサンプルテスト1")]
public void AddTest1()
{
    var result = new SampleClass1().Add(1, 2);
    Assert.Equal(3, result);
}

[Theory(DisplayName = "加算のサンプルテスト2")]
[InlineData(1, 2, 3)]
[InlineData(1, 2, 999)]
public void AddTest2(int in1, int in2, int expected)
{
    var result = new SampleClass1().Add(in1, in2);
    Assert.Equal(expected, result);
}

検証メソッド

メソッド 内容
Assert.Equal 値が一致するか
Assert.True 値が True であるか
Assert.Null 値が NULL であるか
Assert.Throw 指定の例外が発生するか
  • 上記は抜粋。
  • xunit の Assert クラスに定義されている。
// 以下の場合、検証を通過する。
[Fact]
public void Test()
{
    bool? dummy = false;
    Assert.Equal(false, dummy);

    dummy = true;
    Assert.True(dummy);

    dummy = null;
    Assert.Null(dummy);

    Assert.ThrowsAsync<ArgumentNullException>(() => 
    {
        throw new ArgumentNullException("test");
    });
}

ITestOutputHelper インターフェース

  • テストエクスプローラーに文字列を出力できる。
  • 以下のログをテストエクスプローラーに出力できる。
    • テストメソッドのログ
    • プロジェクトのログ(自作処理が少し必要)

テストメソッドのログを出力

public class SampleClass1Test
{
    private readonly ITestOutputHelper helper;

    public SampleClass1Test(ITestOutputHelper helper)
    {
        // XUnit 側で DI してくれる。
        this.helper = helper;
    }

    [Fact]
    public void AddTest()
    {
        helper.WriteLine($"テストを開始 (^^)/");

        var result = new SampleClass1().Add(1, 2);
        Assert.Equal(3, result);

        helper.WriteLine($"テストに成功した <result:{result}>");
    }
}
  • DI で ITestOutputHelper のオブジェクトを取得する。
  • WriteLine メソッドで出力する。

プロジェクトのログを出力

// https://docs.microsoft.com/ja-jp/dotnet/core/extensions/custom-logging-provider
public class XunitLogger : ILogger
{
    private readonly ITestOutputHelper helper;
    private readonly string categoryName;

    public XunitLogger(ITestOutputHelper helper, string categoryName)
    {
        this.helper = helper;
        this.categoryName = categoryName;
    }
    public IDisposable BeginScope<TState>(TState state) => default!;
    public bool IsEnabled(LogLevel logLevel) => true;   // 全レベル有効

    public void Log<TState>(
        LogLevel logLevel, 
        EventId eventId,
        TState state,
        Exception exception,
        Func<TState, Exception, string> formatter)
    {
        if (!IsEnabled(logLevel)) return;
        helper.WriteLine($"{logLevel}, {state}, {categoryName}");
        if (exception != null) helper.WriteLine(exception.ToString());
    }
}

public class XunitLoggerProvider : ILoggerProvider
{
    private readonly ITestOutputHelper helper;
    public XunitLoggerProvider(ITestOutputHelper helper) => this.helper = helper;
    public ILogger CreateLogger(string categoryName) => new XunitLogger(helper, categoryName);
    public void Dispose() { }
}
[Fact]
public void AddTest()
{
    var loggerFactory = new LoggerFactory();
    loggerFactory.AddProvider(new XunitLoggerProvider(helper));
    var logger = loggerFactory.CreateLogger<SampleClass1>();

    // SampleClass1 クラスのコンストラクタでログを記録する。
    var result = new SampleClass1(logger).Add(1, 2);
    Assert.Equal(3, result);
}
  • プロジェクトが ILogger を DI する場合に使用できる。
  • 自作の XunitLoggerProvider により、ログの処理を差し替える。
  • XunitLogger の Log メソッドにより、形式は自由に変更できる。



以上