NDW

アプリ開発やトラブルシューティング等のノウハウ、キャンプや登山の紹介や体験談など。

.NET Core 1. システムエンジニアリング 実装技術

.NET Core(C#): xUnitのAssert使用方法

投稿日:2021年8月8日 更新日:

はじめに

  • 次の環境を使用して動作確認しています。
    OS Windows 10(64ビット)
    IDE Microsoft Visual Studio Community 2019(16.8.5) + C#(8.0)
    パッケージ Microsoft.NET.Test.Sdk 16.10.0
    xunit 2.4.1
    xunit.runner.visualstudio 2.4.3
  • “Assert”や”Assertion”は一般的に主張・表明する旨の意味です。プログラミングにおいては「プログラムの前提条件を示す」ために使用される用語です。ここでは便宜上、「検証」と表現します。
  • 完全なソースコードはこちらで公開しています。
  • モックを使用する場合の例はこちらで紹介しています。

基本的な検証

Assertクラスで提供されるstaticメソッドを使用して検証を行えます。

// 同値
Assert.Equal(1, 1);
Assert.Equal(1, int.Parse("1"));
Assert.Equal("123", "123");
Assert.Equal("123", 123.ToString());
Assert.Equal(new string[] { "a", "b" }, "a,b".Split(','));

// オブジェクト同値
var obj = new string("test");
var obj1 = new string("test");
var obj2 = obj;
// ※深い比較
Assert.Equal(obj, obj1);   // 同一インスタンス
Assert.Equal(obj, obj2);   // 異なるインスタンス(内部値は同一)
// ※浅い比較
Assert.NotSame(obj, obj1); // 同一インスタンス
Assert.Same(obj, obj2);    // 異なるインスタンス(内部値は同一)

// Null値
Assert.Null(null);
Assert.NotNull(new object());

// bool値
Assert.True(true);
Assert.False(false);

// 文字列
Assert.StartsWith("abc", "abcdefg");
Assert.EndsWith("efg", "abcdefg");
  • 基本的な検証はAssert.Equal()で行います。
    第1引数には「期待値」、第2引数は「実行値」(テスト実行した結果)を指定します。
    Assert.Equal( 期待値, 実行値 );

    期待値と実行値は、検証失敗時のメッセージのExpected, Actualで表示されます。

  • 期待値がnullやtrue/false等の特定の値になる場合は、Assert.Null(), NotNull(), True(), False()等のメソッドを使用できます。
  • Assert.Equal()とAssert.Same()の違い
    Equal()は「深い比較」、Same()は「浅い比較」を行う違いがあります。
    Assert.Equal() オブジェクトのEquals()メソッドを実行して同一であることを判定(深い比較)します。
    Assert.Same() 同一のインスタンス(変数の参照先が同一)かどうかを判定(浅い比較)します。
    この性質上、検証の対象は参照型に限られます。char, int等の値型は代入時に常に新しいインスタンス(コピー)が作成されるので、検証が常に失敗します。
  • Equals()というメソッドも提供されていますが、こちらは古いメソッドであり非推奨になっています。

型の検証

Assert.IsAssignableFrom()やAssert.IsType()を使って型を検証できます。

// テスト対象: HttpWebRequest
// - 継承: Object -> MarshalByRefObject -> WebRequest -> HttpWebRequest
// - 実装: ISerializable
object obj = WebRequest.Create("http://www.contoso.com/"); // HttpWebRequestを返却

// 任意のクラス・インターフェイスを継承・実装しているかを判定
Assert.IsAssignableFrom<ISerializable>(obj);
Assert.IsAssignableFrom<WebRequest>(obj);
HttpWebRequest af = Assert.IsAssignableFrom<HttpWebRequest>(obj);
Assert.Equal("http://www.contoso.com/", af.RequestUri.ToString());

// 実行時の型を判定
// (インターフェイスや抽象クラスは実行時の型になりえないので常に失敗)
Assert.IsNotType<ISerializable>(obj); // インターフェイス
Assert.IsNotType<WebRequest>(obj);    // 抽象クラス
Assert.IsNotType<FtpWebRequest>(obj); // 継承していないクラス
HttpWebRequest hr = Assert.IsType<HttpWebRequest>(obj);
Assert.Equal("http://www.contoso.com/", hr.RequestUri.ToString());
  • Assert.IsAssignableFrom()とAssert.IsType()の違い
    IsAssignableFrom()は抽象クラスやインターフェイスの継承・実装の関係性の判定、IsType()は実行時の型を判定します。
    Assert.IsAssignableFrom() 任意のクラス・インターフェイスを継承・実装しているかを判定できます。
    仮パラメータとして抽象クラスやインターフェイスを指定できます。
    Assert.IsType() 実行時の型(実際のクラス)を判定します。
    仮パラメータとして具象クラスを指定する必要があります。抽象クラスやインターフェイスも指定できますが、この場合は常に検証に失敗します。
  • どちらもメソッドも成功時に戻り値として「検証した型にキャストしたオブジェクト」を返却します。後続の検証を行いたい場合はそれを使用できます。

例外の検証

例外がスローされたかどうかはAssert.Throws()やAssert.ThrowsAsync()で検証できます。

// 例外の検証
// "Buffer cannot be null. (Parameter 'buffer')"
var ex1 = Assert.Throws<ArgumentNullException>(
    () => new MemoryStream(null));
Assert.StartsWith("Buffer cannot be null.", ex1.Message);

// 非同期メソッドでの例外の検証
// "Value cannot be null. (Parameter 'destination')"
var ex2 = await Assert.ThrowsAsync<ArgumentNullException>(
    async () => await new MemoryStream().CopyToAsync(null));
Assert.StartsWith("Value cannot be null.", ex2.Message);
  • これらのメソッドではスローされた例外型を検証できますが、例外内容の検証までできません。
  • 想定とは別の原因で同じ例外型がスローされる場合も考えられるので、上記例のようにEqual()やStartWith()等で例外メッセージを検証することをおすすめします。

コレクションの基本的な検証

Assert.Equal()やコレクション向けのAssert.Empty(), Assert.Contains()などのメソッド群では、IEnumerable型を指定できます。そのため、このインターフェイスを実装している配列リストディクショナリなどを検証できます。

// リスト系

Assert.Empty(new int[0]);
var ints = new int[] { 1, 2 };
Assert.Equal(new int[] { 1, 2 }, ints);
Assert.Contains(2, ints);
Assert.DoesNotContain(3, ints);

Assert.Empty(new List<string>());
var strList = new List<string>() { "abc", "123" };
Assert.Equal(new List<string>() { "abc", "123" }, strList);
Assert.Contains("123", strList);
Assert.DoesNotContain("xyz", strList);

var firstElement = Assert.Single(new int[] { 100 });
Assert.Equal(100, firstElement);

// ディクショナリ系

Assert.Empty(new Dictionary<string, string>());
var dic1 = new Dictionary<string, string>() { ["k1"] = "v1", ["k2"] = "v2" };
var dic2 = new Dictionary<string, string>() { ["k1"] = "v1", ["k2"] = "v2" };
Assert.Equal(dic1, dic2);

独自クラスを格納するリストの検証

独自クラスを格納するリストをAssert.Equal()で検証すると失敗します。
Assert.Equal()で検証できるようにするためには、独自クラスでEquals()をオーバーロードするか、Assert.Equal()の第3引数として等価比較クラス(IEqualityComparerを実装したクラス)を指定する必要があります。

// 適切なEquals/GetHashCodeの実装がないリストは検証不可
var data1List1 = new List<LData1>() { new LData1(1, "a"), new LData1(2, "b") };
var data1List2 = new List<LData1>() { new LData1(1, "a"), new LData1(2, "b") };
Assert.NotEqual(data1List1, data1List2);
// IEqualityComparerを実装した等価比較クラスを指定することで検証可
// (ただし、等価比較クラスから判定に必要なフィールドにアクセスできる必要あり。
// 判定にprivateフィールドが必要な場合は判定不可となる。)
Assert.Equal(data1List1, data1List2, new LData1Comparer());

// 適切なEquals/GetHashCodeを実装したリストは検証可
var data2List1 = new List<LData2>() { new LData2(1, "a"), new LData2(2, "b") };
var data2List2 = new List<LData2>() { new LData2(1, "a"), new LData2(2, "b") };
Assert.Equal(data2List1, data2List2);

上記のサンプルで使用しているLData1, LData2の定義は次の通りです。LData1はEquals()をオーバーロードせず、LData2はオーバーロードしています。
Equals()やGetHashCode()の実装は、VisualStudio 2019のリファクタリング機能で簡単に追加できます。

public class LData1
{
    public int Val1 { get; }
    public string Val2 { get; }
    public LData1(int val1, string val2)
    {
        Val1 = val1;
        Val2 = val2;
    }
}
public class LData2 : LData1
{
    public LData2(int val1, string val2) : base(val1, val2) { }

    public override bool Equals(object obj)
        => obj is LData2 data &&
            Val1 == data.Val1 &&
            Val2 == data.Val2;

    public override int GetHashCode()
        => HashCode.Combine(Val1, Val2);
}

サンプルで使用している等価比較クラスLData1Comparerの定義は次の通りです。
IEqualityComparerインターフェイスで要求されるEquals()とGetHashCode()を実装しています。
今回の用途ではGetHashCode()は使用しないので適当な実装にしています。詳細はObject.GetHashCode()のリファレンスをご覧ください。

public class LData1Comparer : IEqualityComparer<LData1>
{
    public bool Equals([AllowNull] LData1 x, [AllowNull] LData1 y)
        => (x == null && y == null)
            || (x != null && y != null
                && x.Val1 == y.Val1
                && x.Val2 == y.Val2);

    public int GetHashCode([DisallowNull] LData1 obj)
        => throw new NotImplementedException();
}

独自クラスを格納するディクショナリの検証

前述のリストと同様、独自クラスを格納するディクショナリをAssert.Equal()で検証すると失敗します。
Assert.Equal()で検証できるようにするためには、独自クラスでEquals()をオーバーロードするか、Assert.Equal()の第3引数として等価比較クラスを指定する必要があります。

// Dictionary<K, V>におけるVに対してオブジェクトを指定する想定。
// 適切なEquals/GetHashCodeの実装がないオブジェクトの場合は検証不可。
var data1Dic1 = new Dictionary<string, DData1>()
{
    ["key1"] = new DData1(1),
    ["key2"] = new DData1(2)
};
var data1Dic2 = new Dictionary<string, DData1>()
{
    ["key1"] = new DData1(1),
    ["key2"] = new DData1(2)
};
Assert.NotEqual(data1Dic1, data1Dic2);
// IEqualityComparerを実装した等価比較クラスを指定することで検証可
Assert.Equal(data1Dic1, data1Dic2, new DicComparer());

// Dictionary<K, V>におけるVに対してオブジェクトを指定する想定。
// 適切なEquals/GetHashCodeを実装したオブジェクトの場合は検証可。
var data2Dic1 = new Dictionary<string, DData2>()
{
    ["key1"] = new DData2(1),
    ["key2"] = new DData2(2)
};
var data2Dic2 = new Dictionary<string, DData2>()
{
    ["key1"] = new DData2(1),
    ["key2"] = new DData2(2)
};
Assert.Equal(data2Dic1, data2Dic2);

上記のサンプルで使用しているDData1, DData2の定義は次の通りです。DData1はEquals()をオーバーロードせず、DData2はオーバーロードしています。
Equals()やGetHashCode()は、VisualStudio 2019のリファクタリング機能で簡単に追加できます。

public class DData1
{
    public int Val { get; }
    public DData1(int val) => Val = val;
}
public class DData2 : DData1
{
    public DData2(int val) : base(val) { }
    public override bool Equals(object obj)
        => obj is DData2 data && Val == data.Val;
    public override int GetHashCode()
        => HashCode.Combine(Val);
}

サンプルで使用している等価比較クラスDicComparerの定義は次の通りです。
前述の「独自クラスを格納するリストの検証」と同様に実装しているので、詳細はそちらをご覧ください。

// Dictionary<string, DData1>に特化した等価比較クラス
// (より汎用的なDictionary<K, V>の定義も可能だが、Vに対応するDData1クラスで
// Equals()の実装が必要となってしまうので、ここでは特化している。)
public class DicComparer : IEqualityComparer<Dictionary<string, DData1>>
{
    public bool Equals(
        [AllowNull] Dictionary<string, DData1> x, [AllowNull] Dictionary<string, DData1> y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;

        // 格納数の検証
        if (x.Count != y.Count) return false;

        // 含まれるキーの検証
        foreach (var xkey in x.Keys)
            if (!y.ContainsKey(xkey)) return false;

        // 含まれる値の検証
        foreach (var xpair in x)
            if (xpair.Value.Val != y[xpair.Key].Val) return false;

        return true;
    }

    public int GetHashCode([DisallowNull] Dictionary<string, DData1> obj)
        => throw new NotImplementedException();
}






-.NET Core, 1. システムエンジニアリング, 実装技術

関連記事

vbaでのエンコード/デコードのサンプル

Excel(vba)で、MD5/SHA-1/SHA-2(SHA-256)の出力、Hex/Base64エンコード/デコードを調べたので備忘録として残します。 動作検証した環境は、Windows10+Of …

ASP.NET Core: IHttpClientFactoryの単純サンプル

IHttpClientの使い方やサンプルの記事を書きましたが、後から見るとちょっと量が多いと感じました。 とりあえず動かしてみたい、概略を知りたい、急いでいる等の人向けに、もっと単純なサンプルを用意し …

PowerPointの削除できない個人情報を消す

PowerPointで「個人情報の削除」を実行するとこで、作成者や会社名等の個人情報を削除できます。しかしながら、特定の項目に入った個人情報については、PowerPointやWindowsの標準機能で …

JIS X 0208, Shift_JIS, Windows-31Jの歴史と違い

文字コードの話は難しそうなイメージがあり、必要になったタイミングでその都度、最小限の知識を習得して対応してきました…が、効率が悪く踏み込んだ話になった時に困る場合もあるので、ここで真面目に …

Visual Studio 2019と開発用DB(LocalDB)

Visual Studio 2019でのASP.NET Coreを使ったシステム開発の標準化を検討している。 開発工程では技術検証、新機能向けのテーブル定義の検討、単体試験等を目的として、開発者が自由 …