はじめに
- 次の環境を使用して動作確認しています。
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メソッドを使用して検証を行えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 同値 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( 期待値, 実行値 );
- 期待値が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()を使って型を検証できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // テスト対象: 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()で検証できます。
1 2 3 4 5 6 7 8 9 10 11 | // 例外の検証 // "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型を指定できます。そのため、このインターフェイスを実装している配列やリスト、ディクショナリなどを検証できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // リスト系 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.Collection()やAssert.All()で検証できます。
1 2 3 4 5 6 7 8 9 10 | var list = new List<TestData>() { new TestData("a", 1, true), new TestData("b", 2, true) }; // 要素毎に異なる検証(全ての要素に対する検証ラムダ式の指定が必須) Assert.Collection(list, e => { Assert.Equal("a", e.Name); Assert.Equal(1, e.Count); Assert.True(e.Succeeded); }, e => { Assert.Equal("b", e.Name); Assert.Equal(2, e.Count); Assert.True(e.Succeeded); } ); // 全ての要素に対する検証(単一の検証ラムダ式のみ指定可能) Assert.All(list, e => Assert.True(e.Succeeded)); |
- コレクションに含まれる要素毎に異なる検証を行う場合は、Assert.Collection()を使用します。N個の要素を検証する場合、要素内容を検証するためのラムダ式をN個指定する必要があります。
- コレクションに含まれる全ての要素に同じ検証を行う場合は、Assert.All()を使用します。要素数に関わらず、検証用ラムダ式を1つ指定します。
上記のサンプルで使用しているTestDataの定義は次の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 | public class TestData { public string Name { get; } public int Count { get; } public bool Succeeded { get; } public TestData(string name, int count, bool succeeded) { Name = name; Count = count; Succeeded = succeeded; } } |
独自クラスを格納するリストの検証
独自クラスを格納するリストをAssert.Equal()で検証すると失敗します。
Assert.Equal()で検証できるようにするためには、独自クラスでEquals()をオーバーロードするか、Assert.Equal()の第3引数として等価比較クラス(IEqualityComparerを実装したクラス)を指定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // 適切な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のリファクタリング機能で簡単に追加できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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()のリファレンスをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 | 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引数として等価比較クラスを指定する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // 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のリファクタリング機能で簡単に追加できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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の定義は次の通りです。
前述の「独自クラスを格納するリストの検証」と同様に実装しているので、詳細はそちらをご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // 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(); } |