如何断言两个列表包含 NUnit 中具有相同公共属性的元素?

Posted

技术标签:

【中文标题】如何断言两个列表包含 NUnit 中具有相同公共属性的元素?【英文标题】:How to assert that two list contains elements with the same public properties in NUnit? 【发布时间】:2012-08-23 16:16:44 【问题描述】:

我想断言两个列表的元素包含我期望的值,例如:

var foundCollection = fooManager.LoadFoo();
var expectedCollection = new List<Foo>() 

    new Foo()  Bar = "a", Bar2 = "b" ,
    new Foo()  Bar = "c", Bar2 = "d" 
;

//assert: I use AreEquivalent since the order does not matter
CollectionAssert.AreEquivalent(expectedCollection, foundCollection);

但是上面的代码不起作用(我猜是因为 .Equals() 不会为具有相同值的不同对象返回 true)。在我的测试中,我只关心公共属性值,而不关心对象是否相等。我能做些什么来表达我的主张?

【问题讨论】:

【参考方案1】:

要对复杂类型执行等价操作,您需要实现 ICMaprable。

http://support.microsoft.com/kb/320727

您也可以使用递归反射,这不太理想。

【讨论】:

你的意思是我必须修改生产代码来实现这个IComparable?是否有不需要修改生产代码的解决方案,例如使用反射或指定我自己的 NUnit 比较器?这只是测试所需要的,对象本身没有可比性 然后作为我的第二个建议,使用反射遍历属性列表并生成值哈希。或者,如果对象是可序列化的,则 JSON 序列化它们并使用字符串比较 如何简单的“JSON序列化”?【参考方案2】:

你尝试过这样的事情吗?

Assert.That(expectedCollection, Is.EquivalentTo(foundCollection))

【讨论】:

这与 CollectionAssert.AreEquivalent 有什么不同?无论如何,两者都不起作用,返回关于对象不相等的类似异常 我认为它与自定义 Foo 对象有关,它不知道如何比较它们,所以在这种情况下,自定义约束可能是解决方案。 是的,这确实是我的怀疑。知道如何创建自定义约束或自定义断言吗?【参考方案3】:

不,NUnit 在当前状态下没有这样的机制。您必须推出自己的断言逻辑。或者作为单独的方法,或者使用Has.All.Matches:

Assert.That(found, Has.All.Matches<Foo>(f => IsInExpected(f, expected)));

private bool IsInExpected(Foo item, IEnumerable<Foo> expected)

    var matchedItem = expected.FirstOrDefault(f => 
        f.Bar1 == item.Bar1 &&
        f.Bar2 == item.Bar2 &&
        f.Bar3 == item.Bar3
    );

    return matchedItem != null;

这当然假设您预先知道所有相关属性(否则,IsInExpected 将不得不求助于反射)并且元素顺序不相关。

(你的假设是正确的,NUnit 的集合断言使用默认的类型比较器,在大多数情况下,用户定义的比较器将是对象的 ReferenceEquals

【讨论】:

【参考方案4】:

一种选择是编写自定义约束来比较项目。这是一篇关于这个主题的好文章:http://www.davidarno.org/2012/07/25/improving-nunit-custom-constraints-with-syntax-helpers/

【讨论】:

【参考方案5】:

重新设计的答案

有一个CollectionAssert.AreEqual(IEnumerable, IEnumerable, IComparer) 重载来断言两个集合以相同的顺序包含相同的对象,使用IComparer 实现来检查对象等价性。

在上述场景中,顺序并不重要。但是,为了充分处理两个集合中存在多个等价对象的情况,有必要首先对每个集合中的对象进行排序,并使用一对一比较来确保等价对象的数量也相同在两个集合中。

Enumerable.OrderBy 提供了一个采用IComparer&lt;T&gt; 参数的重载。为了确保两个集合按相同的顺序排序,或多或少需要标识属性的类型实现IComparable。下面是一个比较器类的示例,它同时实现了IComparerIComparer&lt;Foo&gt; 接口,并且假设在排序时Bar 优先:

public class FooComparer : IComparer, IComparer<Foo>

    public int Compare(object x, object y)
    
        var lhs = x as Foo;
        var rhs = y as Foo;
        if (lhs == null || rhs == null) throw new InvalidOperationException();
        return Compare(lhs, rhs);
    

    public int Compare(Foo x, Foo y)
    
        int temp;
        return (temp = x.Bar.CompareTo(y.Bar)) != 0 ? temp : x.Bar2.CompareTo(y.Bar2);
    

要断言两个集合中的对象相同且数量相同(但不一定以相同的顺序开始),以下几行应该可以解决问题:

var comparer = new FooComparer();
CollectionAssert.AreEqual(
    expectedCollection.OrderBy(foo => foo, comparer), 
    foundCollection.OrderBy(foo => foo, comparer), comparer);    

【讨论】:

实际上,我不想断言订单..关于如何编写帮助方法的任何想法? @LouisRhys 我添加了示例代码,其中两个集合中对象的顺序无关紧要。 如果列表长度不同,使用上述 Any() 会产生问题。如果预期是实际的子集,则测试将通过。例如,预期 = A, B,实际 = A,C,B A,B.Except(A,B,C = 为了允许不同的长度,可以添加对计数的检查或运行两个方向的例外。 @AlanT 你完全正确,很抱歉疏忽。我已经相应地更新了答案。 @Louis Rhys 如果实际或预期中存在重复项目,则会出现问题。使用的集合操作不允许给定项目的倍数。如果可能发生重复,则可以使用 'lhsCount == rhsCount && lhs.Intersect(rhs, equalComparer).Count() == lhsCount;' 比较列表。【参考方案6】:

我建议不要使用反射或任何复杂的东西,它只会增加更多的工作/维护。

序列化对象(我推荐 json)和字符串比较它们。 我不确定您为什么反对订购,但我仍然推荐它,因为它会为每种类型保存自定义比较。

它会自动处理领域对象的变化。

示例(SharpTestsEx for fluent)

using Newtonsoft.Json;
using SharpTestsEx;

JsonConvert.SerializeObject(actual).Should().Be.EqualTo(JsonConvert.SerializeObject(expected));

你可以把它写成一个简单的扩展,让它更具可读性。

   public static class CollectionAssertExtensions
    
        public static void CollectionAreEqual<T>(this IEnumerable<T> actual, IEnumerable<T> expected)
        
            JsonConvert.SerializeObject(actual).Should().Be.EqualTo(JsonConvert.SerializeObject(expected));
        
    

然后使用您的示例调用它:

var foundCollection = fooManager.LoadFoo();
var expectedCollection = new List<Foo>() 

    new Foo()  Bar = "a", Bar2 = "b" ,
    new Foo()  Bar = "c", Bar2 = "d" 
;


foundCollection.CollectionAreEqual(foundCollection);

你会收到这样的断言消息:

...:"a","Bar2":"b","Bar":"d","Bar2":"d"]

...:"a","Bar2":"b","Bar":"c","Bar2":"d"]

...__________________^_____

【讨论】:

【参考方案7】:

使用 Has.All.Matches() 非常适合将 找到 集合与 预期 集合进行比较。但是,不必将 Has.All.Matches() 使用的谓词定义为单独的函数。对于相对简单的比较,谓词可以像这样包含在 lambda 表达式中。

Assert.That(found, Has.All.Matches<Foo>(f => 
    expected.Any(e =>
        f.Bar1 == e.Bar1 &&
        f.Bar2 == e.Bar2 &&
        f.Bar3 == e.Bar3)));

现在,虽然此断言将确保 found 集合中的每个条目也存在于 expected 集合中,但它并不能证明相反,即预期集合包含在 found 集合中。因此,当知道 foundexpected 包含在语义上是等价的(即它们包含相同的语义上等价的条目)很重要时,我们必须添加一个额外的断言。

最简单的选择是添加以下内容。

Assert.AreEqual(found.Count(), expected.Count());

对于那些喜欢更大的锤子的人,可以使用以下断言。

Assert.That(expected, Has.All.Matches<Foo>(e => 
    found.Any(f =>
        e.Bar1 == f.Bar1 &&
        e.Bar2 == f.Bar2 &&
        e.Bar3 == f.Bar3)));

通过将上面的第一个断言与第二个(首选)或第三个断言结合使用,我们现在已经证明这两个集合在语义上是相同的。

【讨论】:

【参考方案8】:

我遇到了类似的问题。列出贡献者,其中包含“评论者”和其他人......我想获得所有的 cmets 并从中派生创作者,但我只对独特的创作者感兴趣。如果有人创建了 50 cmets,我只希望她的名字出现一次。所以我写了一个测试,看看评论者是否在 GetContributors() 结果中。

我可能是错的,但我认为您的后继(我发现这篇文章时的后继)是断言在一个集合中的每个项目中恰好有一个,在另一个集合中找到。

我是这样解决的:

Assert.IsTrue(commenters.All(c => actual.Count(p => p.Id == c.Id) == 1));

如果您还希望结果列表不包含预期之外的其他项目,您也可以比较列表的长度..

Assert.IsTrue(commenters.length == actual.Count());

我希望这会有所帮助,如果是这样,如果您能评价我的回答,我将不胜感激。

【讨论】:

【参考方案9】:

解释如何使用 IComparer 的简单代码

using System.Collections;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CollectionAssert

    [TestClass]
    public class UnitTest1
    
        [TestMethod]
        public void TestMethod1()
        
            IComparer collectionComparer = new CollectionComparer();
            var expected = new List<SomeModel> new SomeModel  Name = "SomeOne", Age = 40, new SomeModelName="SomeOther", Age = 50;
            var actual = new List<SomeModel>  new SomeModel  Name = "SomeOne", Age = 40 , new SomeModel  Name = "SomeOther", Age = 50  ;
            NUnit.Framework.CollectionAssert.AreEqual(expected, actual, collectionComparer);
        
    

    public class SomeModel
    
        public string Name  get; set; 
        public int Age  get; set; 
    

    public class CollectionComparer : IComparer, IComparer<SomeModel>
    
        public int Compare(SomeModel x, SomeModel y)
        
            if(x == null || y == null) return -1;
            return x.Age == y.Age && x.Name == y.Name ? 0 : -1;
        

        public int Compare(object x, object y)
        
            var modelX = x as SomeModel;
            var modelY = y as SomeModel;
            return Compare(modelX, modelY);
        
    

【讨论】:

【参考方案10】:

这解决了我使用来自 NUnitCore 程序集的 NUnit 的 Assertion 类的问题:

AssertArrayEqualsByElements(list1.ToArray(), list2.ToArray());

【讨论】:

以上是关于如何断言两个列表包含 NUnit 中具有相同公共属性的元素?的主要内容,如果未能解决你的问题,请参考以下文章

用 ScalaTest 比较集合内容

流畅的断言。如何验证两个列表至少有一个相同的元素

Swift - 两个具有相同公共结构的框架

如何在需要括号的 NUnit 中编写流畅的约束

NUnit 断言是不是有 ShouldBeEquivalentTo() 替代方案?

如何在 FluentAssertions 中使用方法