Chaining Assertion 1.7.0.1 - 値比較の追加

EqualsやGetHashCodeをオーバーライドするかと言ったら、そういう目的があるなら当然するし、そうでないならしない。という極当たり前なところに落ち着いたりはする。目的ってどういう時かといったら、LINQでDistinctの対象にしたい時とかですかね、まあ、よーするに値で比較したい!時です、まんまですね。

なので、逆にそれ以外の用途であえてこれらをオーバーライドすることはないです。特にテストのためにオーバーライドというのは、はっきしいって、良くないって思ってます。Equalsというのはクラスとして非常に重要な意味のあるところなので、そこにテスト都合が混じりこむのはNGです。

でも、テスト都合で値で比較したかったりは割とあるんですよね。じゃあどうするかって、まあ、ふつーにアサート側で構造比較したほうがいいでしょ、テスト都合なんだから。QUnitなんてdeepEqualが基本で、それが存外使いイイんですよ。

と、いうよくわからない前振りですが、つまるところChaining Assertionに値で比較できるIsStructuralEqualを足しました。

いやあ、一年ぶりの更新です!というか、もう一年前ですかー、早いものだ。もうAssert.ThatをDisるのも忘れてたぐらいに昔の話ですねー、あ、今も当然Assert.ThatとかFluent何ちゃらは嫌いですよ、と、それはさておき。かなり前の話なのでChaining Assertionについておさらい。メソッドチェーンな感じにさらさらっとAssertを書けるテスト補助ライブラリです。詳しくはメソッドチェーン形式のテスト記述ライブラリという1.0出した時の説明をどうぞー。主にMSTestやNUnitに対応しています。勿論NuGetでも入りますのでChainingAssertionで検索を。

今回追加したのはIsStructuralEqual(もしくはIsNotStructuralEqual)で、構造を再帰的に辿って値としての一致で比較します。

// こんなクラスがあるとして
class MyClass
{
    public int IntProperty { get; set; }
    public string StrField;
    public int[] IntArray { get; set; }
    public SubMyClass Sub { get; set; }
}

class SubMyClass
{
    public DateTime Date { get; set; }
}


[TestMethod]
public void TestMethod1()
{
    var mc1 = new MyClass
    {
        IntProperty = 100,
        StrField = "hoge",
        IntArray = new[] { 1, 2, 3, 4, 5 },
        Sub = new SubMyClass
        {
            Date = new DateTime(1999, 12, 31)
        }
    };

    var mc2 = new MyClass
    {
        IntProperty = 100,
        StrField = "hoge",
        IntArray = new[] { 1, 2, 3, 4, 5 },
        Sub = new SubMyClass
        {
            Date = new DateTime(1999, 12, 31)
        }
    };

    mc1.IsNot(mc2); // mc1とmc2は全て同じ値ですが、参照比較では当然違います

    mc1.IsStructuralEqual(mc2); // IsStructuralEqualでは全てのプロパティを再帰的に辿って比較します
}

なんのかんので便利で使っちゃいますね、多用しちゃいますね、きっと。ちなみに、ただたんに値比較したいだけなら、JSONにでもDumpして、文字列一致取ればいいだけなんですけれど。自前で辿ることによって、誤った箇所へのメッセージは割と親切かな、と思います。あとIEquatableの扱いとか型の扱いとか、ただのDumpとは色々若干と違うので、まあ、こちらのほうが望ましい具合な結果が得られると思います。

var mc1 = new MyClass
{
    IntProperty = 100,
    StrField = "hoge",
    IntArray = new[] { 1, 2, 3, 4, 5 },
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};

// 間違い探し!
var mc2 = new MyClass
{
    IntProperty = 100,
    StrField = "hoge",
    IntArray = new[] { 1, 2, 3, 100, 5 }, // 4番目が違う
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};

// 以下のようなエラーメッセージが出ます
// is not structural equal, failed at MyClass.IntArray.[3], actual = 4 expected = 5
mc1.IsStructuralEqual(mc2);

// こんどはStrFieldが違う
var mc3 = new MyClass
{
    IntProperty = 100,
    StrField = "hage",
    IntArray = new[] { 1, 2, 3, 4, 5 },
    Sub = new SubMyClass
    {
        Date = new DateTime(1999, 12, 31)
    }
};

// 以下のようなエラーメッセージが出ます
// is not structural equal, failed at MyClass.StrField, actual = hoge expected = hage
mc1.IsStructuralEqual(mc3);

割と十分、分かりやすい。かな?

コードは結構好き勝手感です。PropertyInfoとFieldInfoは共にMemberInfoを継承してるが、GetValueは同じメソッドシグネチャだけど、MemberInfoに定義されてるわけじゃなくてPropertyInfo, FieldInfoにそれぞれあるから共通でまとめられないよー→dynamicで受ければGetValue使えて大解決。とか、まあふつーのコードではやらないようなことも、UnitTest用だから若干の効率低下は無視でなんでもありで行くよ!というのが割と楽しいですね。

その他

ついでにjsakamotoさんからPull Request来ていたIsInstanceOfでメソッドチェーンできるようになったり(Pull Requestの放置に定評のある私です!というか初めてacceptしたわー)、IsTrueとIsFalseを足した(いやあ、Is(true)ってやっぱ面倒くさかったよ、あはは)りなどしました。

さて、お次はWinRT対応とWindows Phone 8対応を、といったところなのですが、それはそのうち近いうちに!WinRT対応はねえ、リフレクション回りがドサッと変わってるので面倒といえば面倒なんですよねえ。まあ、勉強のためのちょうどいい題材ではあるので、手を付けたいとは思ってるのですけれど。

あとね、正直NUnitはいいとしてもMbUnitやxUnit、SLやWP7に対応させるの超面倒くさい。やりすぎた。これのせいでちょっとした修正ですら大仕事なわけですよ。このメンテコスト最悪すぎる状態がどうにもねえ、それでいてNUnitはともかく、その他なんてほとんど使われてないですからねえ。分かってはいたのですが、こうシンドイと結構限界。というわけで、WP7とSilverlightは削除しました。この二つはもういらないぢゃん?さようなら……。

ああ、あとFakes FrameworkのためのVerifierも入れたいしねえ、やりたいことは割と多いんですが、ニートもこれはこれでいて忙しくて手が回らないのですよー。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive