144

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

オブジェクトは、一連のパブリック プロパティを持つクラスの単なるインスタンスです。プロパティに基づいて 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 と同じ精神です。

4

18 に答える 18

133

テスト目的のためだけに 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 });
}
于 2012-04-27T14:00:00.273 に答える
121

何らかの理由で 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]);
    }
}
于 2008-11-25T17:38:00.883 に答える
104

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

dto.Should().BeEquivalentTo(customer) 

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

于 2011-09-16T05:27:03.513 に答える
55

オブジェクトの .Equals をオーバーライドすると、単体テストで次のように簡単に実行できます。

Assert.AreEqual(LeftObject, RightObject);

もちろん、これはすべての個々の比較を .Equals メソッドに移動することを意味するかもしれませんが、その実装を複数のテストで再利用できるようになり、オブジェクトが自分自身を兄弟と比較できるようにする必要がある場合はおそらく意味があります。

于 2008-11-25T17:33:35.510 に答える
37

テストを有効にするためだけに 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.");
    }
}
于 2009-01-11T02:24:26.817 に答える
10

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

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

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

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

スライ

于 2009-05-01T06:23:30.597 に答える
4

NUnit.Framework.Is.EqualToこれはかなり古いスレッドですが、回答が提案されていない理由があるかどうか疑問に思っていましたNUnit.Framework.Is.NotEqualTo

そのような:

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

Assert.That(LeftObject, Is.Not.EqualTo(RightObject)); 
于 2016-05-12T22:15:46.053 に答える
4

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 への参照を追加する必要がある場合があります。

于 2013-05-29T16:40:09.473 に答える
2

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. 予想されるオブジェクト github
  2. ExpectedObjects の紹介
于 2015-05-24T04:37:21.327 に答える
2

もう 1 つのオプションは、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 ) );
    }
}
于 2013-03-15T14:17:23.030 に答える
1

@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
于 2013-06-11T09:33:42.150 に答える
0

両方のクラスを逆シリアル化し、文字列を比較します。

編集: 完全に動作します。これは 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)

編集 2: 2 つのオブジェクトは同一である可能性がありますが、プロパティがシリアル化される順序は同じではありません。したがって、XML は異なります。ドー!

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

于 2010-11-10T04:30:48.160 に答える