在NUnit中比较两个对象的相等性

158

我想要验证一个对象是否“相等”,这些对象只是一个类的实例,拥有许多公共属性。有没有一种简单的方法使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)

我追求的目标与CollectionEquivalentConstraint相同,其中NUnit验证两个集合的内容是否相同。


请参阅:https://github.com/GregFinzer/Compare-Net-Objects它包含测试扩展,可做您所需的内容:https://github.com/GregFinzer/Compare-Net-Objects/wiki/Test-Extensions - phoenix
20个回答

3

这是一个比较老的帖子,但我想知道为什么没有人提出使用NUnit.Framework.Is.EqualToNUnit.Framework.Is.NotEqualTo的原因?

例如:

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

并且

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

5
因为它没有打印出差异的细节。 - Shrage Smilowitz

2

另一种选择是通过实现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类的合同要求必须重写MatchesWriteDescriptionTo方法(在不匹配的情况下,提供预期值的叙述),但是重写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

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

我已经完成了一个简单的表达式工厂:

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));

这很有用,因为我必须比较这些对象的集合。而且你可以在其他地方使用这个比较器 :) 这里是一个示例链接: https://gist.github.com/Pzixel/b63fea074864892f9aba8ffde312094f

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

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)

编辑二: 这两个对象可以是相同的,但序列化属性的顺序不同。因此,XML 是不同的。天啊!

编辑三: 这确实有效。我在我的测试中使用它。但你必须按照被测试代码添加它们的顺序向集合属性添加项目。


1
序列化?有趣的想法。不过我不确定它在性能方面能否承受得住。 - Michael Haren
不允许您使用给定的精度比较双精度或小数。 - Noctis

0

这里只是一个修改过的答案版本,可以与Moq一起使用:

public static class Helpers {

    public static bool DeepCompare(this object actual, object expected) {
        var properties = expected.GetType().GetProperties();
        foreach (var property in properties) {
            var expectedValue = property.GetValue(expected, null);
            var actualValue = property.GetValue(actual, null);

            if (actualValue == null && expectedValue == null) {
                return true;
            }

            if (actualValue == null || expectedValue == null) {
                return false;
            }

            if (actualValue is IList actualList) {
                if (!AreListsEqual(actualList, (IList)expectedValue)) {
                    return false;
                }
            }
            else if (IsValueType(expectedValue)) {
                if(!Equals(expectedValue, actualValue)) {
                    return false;
                }
            }
            else if (expectedValue is string) {
                return actualValue is string && Equals(expectedValue, actualValue);
            }
            else if (!DeepCompare(expectedValue, actualValue)) {
                return false;
            }
                
        }
        return true;
    }

    private static bool AreListsEqual(IList actualList, IList expectedList) {
        if (actualList == null && expectedList == null) {
            return true;
        }

        if (actualList == null  || expectedList == null) {
            return false;
        }

        if (actualList.Count != expectedList.Count) {
            return false;
        }

        if (actualList.Count == 0) {
            return true;
        }

        var isValueTypeOrString = IsValueType(actualList[0]) || actualList[0] is string;

        if (isValueTypeOrString) {
            for (var i = 0; i < actualList.Count; i++) {
                if (!Equals(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }
        else {
            for (var i = 0; i < actualList.Count; i++) {
                if (!DeepCompare(actualList[i], expectedList[i])) {
                    return false;
                }
            }
        }

        return true;
    }

    private static bool IsValueType(object obj) {
        return obj != null && obj.GetType().IsValueType;
    }

它可以用于在模拟类型上指定设置时匹配对象,当您需要比 It.IsAny<> 更多的内容并且想在所有属性上进行匹配时,就像这样:

_clientsMock.Setup(m => m.SearchClients(
            It.Is<SearchClientsPayload>(x => x.DeepCompare(expectedRequest)))).Returns(expectedResponse);

当然,它可以改进为与可枚举和其他复杂情况一起使用。


0

我知道这是一个非常老的问题,但是 NUnit 仍然不支持原生的功能。然而,如果你喜欢 BDD 风格的测试(就像 Jasmine 一样),你会惊喜地发现 NExpect(https://github.com/fluffynuts/NExpect,从 NuGet 获取)中已经内置了深度相等测试。

(免责声明:我是 NExpect 的作者)


0

Compare-Net-Objects 项目具有内置的测试扩展,支持在 NUnit 中比较嵌套对象。

using KellermanSoftware.CompareNetObjects;

[Test]
public void ShouldCompare_When_Equal_Should__Not_Throw_An_Exception()
{
    //Arrange
    string errorMessage = "Groups should be equal";
    var people1 = new List<Person>() { new Person() { Name = "Joe" } };
    var people2 = new List<Person>() { new Person() { Name = "Joe" } };
    var group1 = new KeyValuePair<string, List<Person>>("People", people1);
    var group2 = new KeyValuePair<string, List<Person>>("People", people2);

    //Assert
    group1.ShouldCompare(group2, errorMessage);
}

-2

将两个字符串转换为字符串并进行比较

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


我猜这个被踩是因为使用了 JavaScript 方法? - derekbaker783

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接