C#: xUnitのAssert.Equalのメッセージカスタマイズ

概要

  • xUnitを使った単体テストでは、実行結果(値)が期待値と一致しているかをAssert.Equal()で検証し、検証に失敗した場合は実行結果と期待値の相違個所が出力されます。検証対象によっては、相違個所のみだと原因個所が分かりづらい場合があるので、相違個所のみではなく全体の出力や出力内容そのものをカスタマイズしてみます。
    Assert.Equal() Failure: Strings differ
                                      ↓ (pos 1000)
    Expected: ···"STUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFG"···
    Actual:   ···"XYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKL"
  • 使用環境は次の通りです。
    ※xUnitを.Net5以上で使用する場合、xUnitの2.2以上が必要です。詳細はxUnitドキュメントの”Test Runner Compatibility”をご覧ください。

    OSWindows 10(64ビット)
    IDEMicrosoft Visual Studio Community 2022(17.6.0)
    言語C#(10.0) + .NET6
    パッケージxunit (2.5.0)
    xunit.runner.visualstudio (2.5.0)
    Microsoft.NET.Test.Sdk (17.7.2)
  • 完全なソースコードはgithubで公開しています。
    • サンプルコードの全てはXunitHowto.csに含まれています。
    • サンプルのコードは全て「null 許容未指定」にしており、これは.csprojファイルのNullableディレクティブで設定しています。お使いの環境でサンプルを使用するとnull許容関連の警告が出力される場合がありますが、適宜変更してください。
    • ここで掲載しているサンプルコードは、可読性を高めるために一部コメントを消しています。

サンプル

検証失敗時に実行結果・期待値全体を出力

  • 検証失敗時に実行結果・期待値全体を出力するサンプルです。
    • 検証自体は自身で実装していますが、検証失敗時はEqualException.ForMismatchedValues(string, string)を使って、xUnitの検証失敗と見なされる例外をスローしています。
    • xUnitの古いバージョン(2.2~2.4)では、このメソッドは存在しません。代わりにEqualException()を使用できます。
    [Fact]
    public void UsingEqualExceptionTest()
    {
        var chars = Enumerable.Range(0, 1024).Select(c => (char)('A' + c % 26)).ToArray();
        var expected = new string(chars);
        var actual = expected.Remove(1000, 1); // 1000文字目を削除
    
        if (!string.Equals(expected, actual))
            // throw new EqualException(expected, actual); // xunit 2.2-2.4
            throw EqualException.ForMismatchedValues(expected, actual); // xunit 2.5+
    
        Assert.Equal(expected, actual);
    }
  • 検証失敗時のメッセージは次の通りです。
    実行結果・期待値ともに全て出力されています。(ただし、1万文字辺りで切り捨てが発生します。)

    Assert.Equal() Failure: Values differ
    Expected: ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJ
    Actual:   ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKL
  • EqualException.ForMismatchedValues()では、次のように第3引数(バナー)を指定することで、追加のメッセージを付与することができます。
    throw EqualException.ForMismatchedValues(expected, actual, "エラーです!"); // xunit 2.5+
    Assert.Equal() Failure: エラーです!
    Expected: ABCDEF...
    Actual:   ABCDEF...

検証失敗時に独自内容を出力

  • より複雑なメッセージを出力したい場合、XunitExceptionを使用します。
    (XunitExceptionは、EqualException等のxUnit系例外の基底クラスになっている。)
  • XunitExceptionを使用して、CSVの各行を検証するサンプルです。
    工夫次第で、検証失敗の原因個所を分かりやすく伝えることでできます。

    [Fact]
    public void UsingXunitExceptionTest()
    {
        var strs = Enumerable.Range(0, 32).Select(c => $"{c},{c + 1},{c + 2}").ToArray();
        var expected = string.Join("\r\n", strs);
        var actual = expected.Remove(98, 1);
    
        AssertCsvEqual(expected, actual);
    }
    private static void AssertCsvEqual(string expected, string actual)
    {
        if (string.Equals(expected, actual)) return;
    
        // 実際には、null考慮や、行数の不一致等の考慮が必要です。
        var exps = expected.Split("\r\n");
        var acts = actual.Split("\r\n");
        var sb = new StringBuilder();
        for (var i = 0; i < exps.Length; i++)
        {
            var e = i < exps.Length ? exps[i] : null;
            var a = i < acts.Length ? acts[i] : null;
            var matched = string.Equals(e, a) ? "--" : "NG";
            sb.AppendLine($"{matched}: \"{e}\" <-> \"{a}\"");
        }
        throw new XunitException(sb.ToString());
    }
  • 実行結果は次の通りです。
    --: "0,1,2" <-> "0,1,2"
    --: "1,2,3" <-> "1,2,3"
        ...(省略)...
    --: "11,12,13" <-> "11,12,13"
    NG: "12,13,14" <-> "12,1314"
    --: "13,14,15" <-> "13,14,15"
        ...(省略)...
    --: "31,32,33" <-> "31,32,33"

参考

Assertメソッドの拡張方法

お手軽に拡張したい場合、プロジェクト独自のAssertクラスを定義した方が早いと思います。

  • C#の拡張メソッドでは難しい。
    拡張メソッドのリファレンスには次の記載があります。拡張メソッドはインスタンスを前提としているので、Assert.Equal()のように静的なメソッドは追加できません。

    拡張メソッドは、静的メソッドとして定義しますが、インスタンス メソッドの構文を使用して呼び出します。
    (Extension methods are defined as static methods but are called by using instance method syntax.)

  • ソースパッケージの使用を推奨。
    xUnitのパッケージ使用のドキュメントを見ると、「Assertクラスを拡張したい場合、代わりにAssertソースパッケージ(xunit.assert.source)の使用を考慮すべき」旨の記載があります。

    If you want to extend the Assert class, you should consider using the xunit.assert.source package instead.