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

22

我希望断言两个列表的元素包含了我期望的值,类似于:

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)。在我的测试中,我只关心公共属性的值,而不是对象是否相等。我该怎么做才能做出断言?

10个回答

23

修改后的答案

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

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

Enumerable.OrderBy 提供了一个重载函数,它接受一个IComparer<T>参数。为了确保两个集合按照相同的顺序排序,需要更多或更少地要求用于标识属性的类型实现IComparable。下面是一个实现了IComparerIComparer<Foo>接口的比较器类的示例,其中假设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);    

1
实际上,我不想断言顺序... 有关于如何编写辅助方法的想法吗? - Louis Rhys
2
@LouisRhys 我已经添加了示例代码,其中两个集合中对象的顺序并不重要。 - Anders Gustafsson
1
如果列表的长度不同,使用上述任何()将会出现问题。如果期望值是实际值的子集,则测试将通过。例如:expected = {A, B},actual = {A,C,B},{A,B}.Except({A,B,C}) = {}。为了允许长度不同,可以添加计数检查或在两个方向上运行except。 - AlanT
@AlanT 您说得非常正确,抱歉疏忽了。我已经相应地更新了答案。 - Anders Gustafsson
1
@Louis Rhys 如果实际或期望中有重复项,则会出现问题。使用的集合操作不允许具有多个给定项。如果可能存在重复,则可以使用“lhsCount == rhsCount && lhs.Intersect(rhs, equalityComparer).Count() == lhsCount;”比较列表。 - AlanT
显示剩余3条评论

7
目前,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)。

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集合中,但是它并不能证明相反的情况,即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)));

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

4

你尝试过像这样的东西吗?

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

1
这与CollectionAssert.AreEquivalent有什么不同吗?无论哪种方法都不起作用,都会返回关于对象不相等的类似异常。 - Louis Rhys
我认为这与自定义的Foo对象有关,它不知道如何比较它们,因此在这种情况下,自定义约束可能是解决方案。 - Erwin
是的,我也怀疑那就是问题所在。你有什么想法如何创建自定义约束或自定义断言吗? - Louis Rhys

2
我有一个类似的问题。列出贡献者,其中包括“评论者”和其他人...我想获取所有的评论,并从中推导出创作者,但我只对唯一的创作者感兴趣。如果有人创建了50个评论,我只想让她的名字出现一次。因此,我编写了一个测试来查看评论者是否在GetContributors()结果中。
我可能错了,但我认为你想要的是(当我发现这篇文章时)断言在一个集合中有每个项目恰好一个,而这些项目在另一个集合中找到。
我这样解决:
Assert.IsTrue(commenters.All(c => actual.Count(p => p.Id == c.Id) == 1));

如果你想让结果列表中不包含其他预期之外的项目,你也可以比较列表的长度。
Assert.IsTrue(commenters.length == actual.Count());

我希望这篇翻译能对您有所帮助。如果是这样的话,如果您可以给我的回答评分,我将非常感激。


1

如果要在复杂类型上执行等价操作,您需要实现IComparable接口。

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

或者您可以使用递归反射,但这不是最理想的选择。


你的意思是我必须修改生产代码来实现这个IComparable吗?有没有不需要修改生产代码的解决方案,比如使用反射或指定自己的比较器给NUnit?这只是为了测试需要,对象本身并没有可比性。 - Louis Rhys
然后,作为我的第二个建议,使用反射迭代属性列表并生成值哈希。或者,如果对象是可序列化的,则将它们进行JSON序列化并使用字符串比较。 - Slappy
如何以简单的方式进行“JSON序列化”? - Louis Rhys

1

1
我建议不要使用反射或任何复杂的东西,这只会增加更多的工作/维护。
序列化对象(我推荐使用json)并进行字符串比较。我不确定你为什么反对按顺序排序,但我仍然建议这样做,因为它将为每种类型保存自定义比较。
而且它可以自动适应领域对象的更改。
示例(SharpTestsEx用于流畅)
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);

You'll get an assert message like so:

你将收到以下的断言信息:

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

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

..._______________^_____


0

简单的代码,解释如何使用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 SomeModel{Name="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);
        }
    }
}

0
这是我的解决方案,使用了NUnit的Assertion类和NUnitCore程序集:
AssertArrayEqualsByElements(list1.ToArray(), list2.ToArray());

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