web-dev-qa-db-ja.com

NUnitの2つのオブジェクト間の同等性を比較する

あるオブジェクトが別のオブジェクトと「等しい」と断言しようとしています。

オブジェクトは、多数のパブリックプロパティを持つクラスのインスタンスです。 NUnitにプロパティに基づいて同等性を表明させる簡単な方法はありますか?

これは私の現在の解決策ですが、もっと良いものがあると思います。

Assert.AreEqual(LeftObject.Property1, RightObject.Property1)
Assert.AreEqual(LeftObject.Property2, RightObject.Property2)
Assert.AreEqual(LeftObject.Property3, RightObject.Property3)
...
Assert.AreEqual(LeftObject.PropertyN, RightObject.PropertyN)

私が目指しているのは、NUnitが2つのコレクションの内容が同一であることを検証するCollectionEquivalentConstraintと同じ精神です。

115
Michael Haren

オブジェクトの.Equalsをオーバーライドし、単体テストでこれを簡単に行うことができます。

Assert.AreEqual(LeftObject, RightObject);

もちろん、これは個々の比較をすべて.Equalsメソッドに移動することを意味する場合がありますが、その実装を複数のテストに再利用できます。

何らかの理由でEqualsをオーバーライドできない場合、リフレクションによってパブリックプロパティを反復処理し、各プロパティをアサートするヘルパーメソッドを構築できます。このようなもの:

public static class AssertEx
{
    public static void PropertyValuesAreEquals(object actual, object expected)
    {
        PropertyInfo[] properties = expected.GetType().GetProperties();
        foreach (PropertyInfo property in properties)
        {
            object expectedValue = property.GetValue(expected, null);
            object actualValue = property.GetValue(actual, null);

            if (actualValue is IList)
                AssertListsAreEquals(property, (IList)actualValue, (IList)expectedValue);
            else if (!Equals(expectedValue, actualValue))
                Assert.Fail("Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue);
        }
    }

    private static void AssertListsAreEquals(PropertyInfo property, IList actualList, IList expectedList)
    {
        if (actualList.Count != expectedList.Count)
            Assert.Fail("Property {0}.{1} does not match. Expected IList containing {2} elements but was IList containing {3} elements", property.PropertyType.Name, property.Name, expectedList.Count, actualList.Count);

        for (int i = 0; i < actualList.Count; i++)
            if (!Equals(actualList[i], expectedList[i]))
                Assert.Fail("Property {0}.{1} does not match. Expected IList with element {1} equals to {2} but was IList with element {1} equals to {3}", property.PropertyType.Name, property.Name, expectedList[i], actualList[i]);
    }
}
115
Juanma

テスト目的だけでEqualsをオーバーライドしないでください。退屈で、ドメインロジックに影響します。代わりに、

JSONを使用してオブジェクトのデータを比較する

オブジェクトに追加のロジックはありません。テスト用の追加タスクはありません。

次の簡単な方法を使用してください。

public static void AreEqualByJson(object expected, object actual)
{
    var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    var expectedJson = serializer.Serialize(expected);
    var actualJson = serializer.Serialize(actual);
    Assert.AreEqual(expectedJson, actualJson);
}

うまくいくようです。テストランナーの結果情報には、含まれているJSON文字列の比較(オブジェクトグラフ)が表示されるため、何が問題なのかを直接確認できます。

また注意してください!より大きな複雑なオブジェクトがあり、それらの一部を比較したい場合は、(シーケンスデータにLINQを使用します )上記のメソッドで使用する匿名オブジェクトを作成します。

public void SomeTest()
{
    var expect = new { PropA = 12, PropB = 14 };
    var sut = loc.Resolve<SomeSvc>();
    var bigObjectResult = sut.Execute(); // This will return a big object with loads of properties 
    AssExt.AreEqualByJson(expect, new { bigObjectResult.PropA, bigObjectResult.PropB });
}
103
Max

FluentAssertionsライブラリをお試しください:

dto.ShouldHave(). AllProperties().EqualTo(customer);

http://www.fluentassertions.com/

NuGetを使用してインストールすることもできます。

81
dkl

テストを有効にするためだけにEqualsをオーバーライドしないことを好みます。 Equalsをオーバーライドする場合、GetHashCodeもオーバーライドする必要があることを忘れないでください。たとえば、辞書でオブジェクトを使用している場合は、予期しない結果が生じる可能性があります。

上記のリフレクションアプローチは、将来のプロパティの追加に対応するので気に入っています。

ただし、すばやく簡単な解決策としては、オブジェクトが等しいかどうかをテストするヘルパーメソッドを作成するか、テスト専用のクラスにIEqualityComparerを実装するのが最も簡単です。 IEqualityComparerソリューションを使用する場合、GetHashCodeの実装を気にする必要はありません。例えば:

// Sample class.  This would be in your main Assembly.
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Unit tests
[TestFixture]
public class PersonTests
{
    private class PersonComparer : IEqualityComparer<Person>
    {
        public bool Equals(Person x, Person y)
        {
            if (x == null && y == null)
            {
                return true;
            }

            if (x == null || y == null)
            {
                return false;
            }

            return (x.Name == y.Name) && (x.Age == y.Age);
        }

        public int GetHashCode(Person obj)
        {
            throw new NotImplementedException();
        }
    }

    [Test]
    public void Test_PersonComparer()
    {
        Person p1 = new Person { Name = "Tom", Age = 20 }; // Control data

        Person p2 = new Person { Name = "Tom", Age = 20 }; // Same as control
        Person p3 = new Person { Name = "Tom", Age = 30 }; // Different age
        Person p4 = new Person { Name = "Bob", Age = 20 }; // Different name.

        Assert.IsTrue(new PersonComparer().Equals(p1, p2), "People have same values");
        Assert.IsFalse(new PersonComparer().Equals(p1, p3), "People have different ages.");
        Assert.IsFalse(new PersonComparer().Equals(p1, p4), "People have different names.");
    }
}
34
Chris Yoxall

ここで言及したいくつかのアプローチを試しました。ほとんどの場合、オブジェクトをシリアル化し、文字列を比較します。非常に簡単で一般的に非常に効果的ですが、障害が発生すると少し短くなり、次のようなことが報告されます:

_Expected string length 2326 but was 2342. Strings differ at index 1729.
_

違いがどこにあるかを把握することは、控えめに言っても痛みです。

FluentAssertions ' オブジェクトグラフ比較 (つまりa.ShouldBeEquivalentTo(b))を使用すると、次のように戻ります。

_Expected property Name to be "Foo" but found "Bar"
_

それはずっといいです。 Get FluentAssertions 今、あなたは後で喜んでいるでしょう(そして、これを賛成する場合は、賛成してください dkl's answer FluentAssertionsが最初に提案された場所)。

13
Todd Menier

私はChrisYoxallに同意します-純粋にテスト目的でメインコードにEqualsを実装するのは良くありません。

一部のアプリケーションロジックでEqualsが必要なためにEqualsを実装している場合は問題ありませんが、純粋なテスト専用のコードを煩雑にしないでください(テストのために同じものをチェックするセマンティクスは、アプリで必要なものとは異なる場合があります)。

要するに、テスト専用のコードをクラスに入れないでください。

ほとんどのクラスでは、リフレクションを使用したプロパティの単純な浅い比較で十分ですが、オブジェクトに複雑なプロパティがある場合は再帰する必要があります。参照を参照する場合、循環参照などに注意してください。

スライ

9
Sly Gryphon

プロパティの制約 、NUnit 2.4.2に追加され、OPの元のソリューションよりも読みやすいソリューションが可能になり、より優れた障害メッセージが生成されます。決して一般的ではありませんが、あまりにも多くのクラスに対してそれを行う必要がない場合、それは非常に適切なソリューションです。

Assert.That(ActualObject, Has.Property("Prop1").EqualTo(ExpectedObject.Prop1)
                          & Has.Property("Prop2").EqualTo(ExpectedObject.Prop2)
                          & Has.Property("Prop3").EqualTo(ExpectedObject.Prop3)
                          // ...

Equalsの実装ほど汎用的ではありませんが、失敗メッセージよりもはるかに優れた失敗メッセージを提供します

Assert.AreEqual(ExpectedObject, ActualObject);
5
Paul Hicks

Max WikstromのJSONソリューション(上記)は、私にとって最も理にかなっています。それは、短く、クリーンで、最も重要なことです。個人的には、JSON変換を個別のメソッドとして実装し、このようにユニットテスト内にアサートを戻したいと思いますが...

便利な方法:

public string GetObjectAsJson(object obj)
    {
        System.Web.Script.Serialization.JavaScriptSerializer oSerializer = new System.Web.Script.Serialization.JavaScriptSerializer();
        return oSerializer.Serialize(obj);
    }

単体テスト :

public void GetDimensionsFromImageTest()
        {
            Image Image = new Bitmap(10, 10);
            ImageHelpers_Accessor.ImageDimensions expected = new ImageHelpers_Accessor.ImageDimensions(10,10);

            ImageHelpers_Accessor.ImageDimensions actual;
            actual = ImageHelpers_Accessor.GetDimensionsFromImage(Image);

            /*USING IT HERE >>>*/
            Assert.AreEqual(GetObjectAsJson(expected), GetObjectAsJson(actual));
        }

参考-ソリューションにSystem.Web.Extensionsへの参照を追加する必要がある場合があります。

3
samaspin

これはかなり古いスレッドですが、答えが提案されなかった理由があるのではないかと思っていましたNUnit.Framework.Is.EqualToおよびNUnit.Framework.Is.NotEqualTo

といった:

Assert.That(LeftObject, Is.EqualTo(RightObject)); 

そして

Assert.That(LeftObject, Is.Not.EqualTo(RightObject)); 
1
user2315856

NugetからExpectedObjectsをインストールするだけで、2つのオブジェクトのプロパティ値、コレクションの各オブジェクト値、2つの合成オブジェクトの値、および匿名型によるプロパティ値の部分比較を簡単に比較できます。

Githubにはいくつかの例があります: https://github.com/hatelove/CompareObjectEquals

オブジェクトを比較するシナリオを含む例を次に示します。

    [TestMethod]
    public void Test_Person_Equals_with_ExpectedObjects()
    {
        //use extension method ToExpectedObject() from using ExpectedObjects namespace to project Person to ExpectedObject
        var expected = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
        };

        //use ShouldEqual to compare expected and actual instance, if they are not equal, it will throw a System.Exception and its message includes what properties were not match our expectation.
        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_PersonCollection_Equals_with_ExpectedObjects()
    {
        //collection just invoke extension method: ToExpectedObject() to project Collection<Person> to ExpectedObject too
        var expected = new List<Person>
        {
            new Person { Id=1, Name="A",Age=10},
            new Person { Id=2, Name="B",Age=20},
            new Person { Id=3, Name="C",Age=30},
        }.ToExpectedObject();

        var actual = new List<Person>
        {
            new Person { Id=1, Name="A",Age=10},
            new Person { Id=2, Name="B",Age=20},
            new Person { Id=3, Name="C",Age=30},
        };

        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_ComposedPerson_Equals_with_ExpectedObjects()
    {
        //ExpectedObject will compare each value of property recursively, so composed type also simply compare equals.
        var expected = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "A",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        };

        expected.ShouldEqual(actual);
    }

    [TestMethod]
    public void Test_PartialCompare_Person_Equals_with_ExpectedObjects()
    {
        //when partial comparing, you need to use anonymous type too. Because only anonymous type can dynamic define only a few properties should be assign.
        var expected = new
        {
            Id = 1,
            Age = 10,
            Order = new { Id = 91 }, // composed type should be used anonymous type too, only compare properties. If you trace ExpectedObjects's source code, you will find it invoke config.IgnoreType() first.
        }.ToExpectedObject();

        var actual = new Person
        {
            Id = 1,
            Name = "B",
            Age = 10,
            Order = new Order { Id = 91, Price = 910 },
        };

        // partial comparing use ShouldMatch(), rather than ShouldEqual()
        expected.ShouldMatch(actual);
    }

参照:

  1. ExpectedObjects github
  2. ExpectedObjectsの紹介
1
In91

https://github.com/kbilsted/StatePrinter は、簡単な単体テストを作成する目的で、オブジェクトグラフを文字列表現にダンプするために特別に作成されました。

  • 適切にエスケープされた文字列を簡単にコピーアンドペーストしてテストに出力し、それを修正するAssertメソッドが付属しています。
  • Unittestを自動的に書き換えることができます
  • すべての単体テストフレームワークと統合します
  • JSONシリアル化とは異なり、循環参照がサポートされています
  • 簡単にフィルタリングできるため、タイプの一部のみがダンプされます

与えられた

class A
{
  public DateTime X;
  public DateTime Y { get; set; }
  public string Name;
}

タイプセーフな方法で、Visual Studioのオートコンプリートを使用して、フィールドを含めるか除外することができます。

  var printer = new Stateprinter();
  printer.Configuration.Projectionharvester().Exclude<A>(x => x.X, x => x.Y);

  var sut = new A { X = DateTime.Now, Name = "Charly" };

  var expected = @"new A(){ Name = ""Charly""}";
  printer.Assert.PrintIsSame(expected, sut);
1
Carlo V. Dango

別のオプションは、NUnit抽象Constraintクラスを実装してカスタム制約を記述することです。少しの構文糖を提供するヘルパークラスを使用すると、結果のテストコードは快適で簡潔で読みやすいものになります。

Assert.That( LeftObject, PortfolioState.Matches( RightObject ) ); 

極端な例として、「読み取り専用」メンバーを持ち、IEquatableではないクラスを考えてみましょう。テストしたいクラスを変更することはできません。

public class Portfolio // Somewhat daft class for pedagogic purposes...
{
    // Cannot be instanitated externally, instead has two 'factory' methods
    private Portfolio(){ }

    // Immutable properties
    public string Property1 { get; private set; }
    public string Property2 { get; private set; }  // Cannot be accessed externally
    public string Property3 { get; private set; }  // Cannot be accessed externally

    // 'Factory' method 1
    public static Portfolio GetPortfolio(string p1, string p2, string p3)
    {
        return new Portfolio() 
        { 
            Property1 = p1, 
            Property2 = p2, 
            Property3 = p3 
        };
    }

    // 'Factory' method 2
    public static Portfolio GetDefault()
    {
        return new Portfolio() 
        { 
            Property1 = "{{NONE}}", 
            Property2 = "{{NONE}}", 
            Property3 = "{{NONE}}" 
        };
    }
}

Constraintクラスのコントラクトでは、MatchesおよびWriteDescriptionToをオーバーライドする必要があります(不一致の場合、期待される値の物語)が、WriteActualValueTo(実際の値の説明)は理にかなっています:

public class PortfolioEqualityConstraint : Constraint
{
    Portfolio expected;
    string expectedMessage = "";
    string actualMessage = "";

    public PortfolioEqualityConstraint(Portfolio expected)
    {
        this.expected = expected;
    }

    public override bool Matches(object actual)
    {
        if ( actual == null && expected == null ) return true;
        if ( !(actual is Portfolio) )
        { 
            expectedMessage = "<Portfolio>";
            actualMessage = "null";
            return false;
        }
        return Matches((Portfolio)actual);
    }

    private bool Matches(Portfolio actual)
    {
        if ( expected == null && actual != null )
        {
            expectedMessage = "null";
            expectedMessage = "non-null";
            return false;
        }
        if ( ReferenceEquals(expected, actual) ) return true;

        if ( !( expected.Property1.Equals(actual.Property1)
                 && expected.Property2.Equals(actual.Property2) 
                 && expected.Property3.Equals(actual.Property3) ) )
        {
            expectedMessage = expected.ToStringForTest();
            actualMessage = actual.ToStringForTest();
            return false;
        }
        return true;
    }

    public override void WriteDescriptionTo(MessageWriter writer)
    {
        writer.WriteExpectedValue(expectedMessage);
    }
    public override void WriteActualValueTo(MessageWriter writer)
    {
        writer.WriteExpectedValue(actualMessage);
    }
}

プラスヘルパークラス:

public static class PortfolioState
{
    public static PortfolioEqualityConstraint Matches(Portfolio expected)
    {
        return new PortfolioEqualityConstraint(expected);
    }

    public static string ToStringForTest(this Portfolio source)
    {
        return String.Format("Property1 = {0}, Property2 = {1}, Property3 = {2}.", 
            source.Property1, source.Property2, source.Property3 );
    }
}

使用例:

[TestFixture]
class PortfolioTests
{
    [Test]
    public void TestPortfolioEquality()
    {
        Portfolio LeftObject 
            = Portfolio.GetDefault();
        Portfolio RightObject 
            = Portfolio.GetPortfolio("{{GNOME}}", "{{NONE}}", "{{NONE}}");

        Assert.That( LeftObject, PortfolioState.Matches( RightObject ) );
    }
}
1
onedaywhen

@Juanmaの答えに基づいて作成します。ただし、これは単体テストアサーションでは実装しないでください。これは、状況によってはテスト以外のコードで非常によく使用できるユーティリティです。

私はこの問題に関する記事を書きました http://timoch.com/blog/2013/06/unit-test-equality-is-not-domain-equality/

私の提案は次のとおりです。

/// <summary>
/// Returns the names of the properties that are not equal on a and b.
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns>An array of names of properties with distinct 
///          values or null if a and b are null or not of the same type
/// </returns>
public static string[] GetDistinctProperties(object a, object b) {
    if (object.ReferenceEquals(a, b))
        return null;
    if (a == null)
        return null;
    if (b == null)
        return null;

    var aType = a.GetType();
    var bType = b.GetType();

    if (aType != bType)
        return null;

    var props = aType.GetProperties();

    if (props.Any(prop => prop.GetIndexParameters().Length != 0))
        throw new ArgumentException("Types with index properties not supported");

    return props
        .Where(prop => !Equals(prop.GetValue(a, null), prop.GetValue(b, null)))
        .Select(prop => prop.Name).ToArray();
} 

NUnitでこれを使用する

Expect(ReflectionUtils.GetDistinctProperties(tile, got), Empty);

不一致の場合、次のメッセージが生成されます。

Expected: <empty>
But was:  < "MagmaLevel" >
at NUnit.Framework.Assert.That(Object actual, IResolveConstraint expression, String message, Object[] args)
at Undermine.Engine.Tests.TileMaps.BasicTileMapTests.BasicOperations() in BasicTileMapTests.cs: line 29
1
TiMoch

次のリンクをご覧ください。コードプロジェクトからのソリューションであり、私も使用しました。オブジェクトを比較するのにうまく機能します。

http://www.codeproject.com/Articles/22709/Testing-Equality-of-Two-Objects?msg=5189539#xx5189539xx

1
kanika

これは本当に古い質問ですが、NUnitはまだこれをネイティブにサポートしていません。ただし、BDDスタイルのテスト(ala Jasmine)が好きなら、NExpect( https://github.com/fluffynuts/NExpect 、NuGetから取得します)、そこにディープエクイティテストが焼き付けられています。

(免責事項:私はNExpectの著者です)

0
daf

両方のクラスをデシリアライズし、文字列を比較します。

編集:完全に動作します。これはNUnitから取得した出力です。

Test 'Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test("ApprovedRatingInDb")' failed:
  Expected string length 2841 but was 5034. Strings differ at index 443.
  Expected: "...taClasses" />\r\n  <ContactMedia />\r\n  <Party i:nil="true" /..."
  But was:  "...taClasses" />\r\n  <ContactMedia>\r\n    <ContactMedium z:Id="..."
  ----------------------------------------------^
 TranslateEaiCustomerToDomain_Tests.cs(201,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.Assert_CustomersAreEqual(Customer expectedCustomer, Customer actualCustomer)
 TranslateEaiCustomerToDomain_Tests.cs(114,0): at Telecom.SDP.SBO.App.Customer.Translator.UnitTests.TranslateEaiCustomerToDomain_Tests.TranslateNew_GivenEaiCustomer_ShouldTranslateToDomainCustomer_Test(String custRatingScenario)

EDIT TWO: 2つのオブジェクトは同一でもかまいませんが、プロパティがシリアル化される順序は同じではありません。したがって、XMLは異なります。 DOH!

EDIT THREE:これは機能します。私はテストでそれを使用しています。ただし、テスト対象のコードが追加する順序で、コレクションプロパティにアイテムを追加する必要があります。

0
Casey Burns

簡単な式ファクトリを書くことで終わりました:

public static class AllFieldsEqualityComprision<T>
{
    public static Comparison<T> Instance { get; } = GetInstance();

    private static Comparison<T> GetInstance()
    {
        var type = typeof(T);
        ParameterExpression[] parameters =
        {
            Expression.Parameter(type, "x"),
            Expression.Parameter(type, "y")
        };
        var result = type.GetProperties().Aggregate<PropertyInfo, Expression>(
            Expression.Constant(true),
            (acc, prop) =>
                Expression.And(acc,
                    Expression.Equal(
                        Expression.Property(parameters[0], prop.Name),
                        Expression.Property(parameters[1], prop.Name))));
        var areEqualExpression = Expression.Condition(result, Expression.Constant(0), Expression.Constant(1));
        return Expression.Lambda<Comparison<T>>(areEqualExpression, parameters).Compile();
    }
}

それを使用するだけです:

Assert.That(
    expectedCollection, 
    Is.EqualTo(actualCollection)
      .Using(AllFieldsEqualityComprision<BusinessCategoryResponse>.Instance));

このようなオブジェクトのコレクションを比較する必要があるため、非常に便利です。そして、あなたは他のどこかでこの比較を使用することができます:)

Gistと例を示します: https://Gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

0
Alex Zhukovskiy

2つの文字列を文字列化して比較する

Assert.AreEqual(JSON.stringify(LeftObject)、JSON.stringify(RightObject))

0
jmtt89