How to assert that two list contains elements with the same public properties in NUnit?

36,366

Solution 1

REWORKED ANSWER

There is a CollectionAssert.AreEqual(IEnumerable, IEnumerable, IComparer) overload to assert that two collections contain the same objects in the same order, using an IComparer implementation to check the object equivalence.

In the scenario described above, the order is not important. However, to sufficiently handle also the situation where there are multiple equivalent objects in the two collections, it becomes necessary to first order the objects in each collection and use one-by-one comparison to ensure that also the number of equivalent objects are the same in the two collections.

Enumerable.OrderBy provides an overload that takes an IComparer<T> argument. To ensure that the two collections are sorted in the same order, it is more or less required that the types of the identifying properties implement IComparable. Here is an example of a comparer class that implements both the IComparer and IComparer<Foo> interfaces, and where it is assumed that Bar takes precedence when ordering:

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

To assert that the objects in the two collections are the same and comes in equal numbers (but not necessarily in the same order to begin with), the following lines should do the trick:

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

Solution 2

No, NUnit has no such mechanism as of current state. You'll have to roll your own assertion logic. Either as separate method, or utilizing 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;
}

This of course assumes you know all relevant properties upfront (otherwise, IsInExpected will have to resort to reflection) and that element order is not relevant.

(And your assumption was correct, NUnit's collection asserts use default comparers for types, which in most cases of user defined ones will be object's ReferenceEquals)

Solution 3

Using Has.All.Matches() works very well for comparing a found collection to the expected collection. However, it is not necessary to define the predicate used by Has.All.Matches() as a separate function. For relatively simple comparisons, the predicate can be included as part of the lambda expression like this.

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

Now, while this assertion will ensure that every entry in the found collection also exists in the expected collection, it does not prove the reverse, namely that every entry in the expected collection is contained in the found collection. So, when it is important to know that found and expected contain are semantically equivalent (i.e., they contain the same semantically equivalent entries), we must add an additional assertion.

The simplest choice is to add the following.

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

For those who prefer a bigger hammer, the following assertion could be used instead.

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

By using the first assertion above in conjunction with either the second (preferred) or third assertion, we have now proven that the two collections are semantically the same.

Solution 4

Have you tried something like this?

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

Solution 5

I had a similar problem. Listing contributors, which contains "commenters" and other ppl... I want to get all the comments and from that derive the creators, but I'm ofc only interested in unique creators. If someone created 50 comments I only want her name to appear once. So I write a test to see that the commenters are int the GetContributors() result.

I may be wrong, but what I think your after (what I was after when I found this post) is to assert that there are exactly one of each item in one collection, found in another collection.

I solved this like so:

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

If you also want the resulting list not to contain other items than expected you could just compare the length of the lists as well..

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

I hope this is helpful, if so, I'd be very grateful if you would rate my answer.

Share:
36,366
Louis Rhys
Author by

Louis Rhys

trying to learn and help others learn :)

Updated on July 19, 2022

Comments

  • Louis Rhys
    Louis Rhys almost 2 years

    I want to assert that the elements of two list contains values that I expected, something like:

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

    However the above code will not work (I guess because .Equals() does not return true for different objects with the same value). In my test, I only care about the public property values, not whether the objects are equal. What can I do to make my assertion?

  • Louis Rhys
    Louis Rhys over 11 years
    You mean I have to modify the production code to implement this IComparable? Is there a solution that doesn't require modifying the production code, like using reflection, or specifying my own comparer to NUnit? This is just needed for testing, the object itself doesn't make sense to be comparable
  • Slappy
    Slappy over 11 years
    Then as my second recommentation goes, iterate through the property list using reflection and generate a value hash. Alternatively if the objects are serlializable, JSON serialize them and use string comparison
  • Louis Rhys
    Louis Rhys over 11 years
    how to "JSON serialize" in a simple way?
  • Louis Rhys
    Louis Rhys over 11 years
    is that different from CollectionAssert.AreEquivalent? anyway both does not work, returning similar exception about objects not being equal
  • Erwin
    Erwin over 11 years
    I think it has to do with the custom Foo object it doesn't know how to compare those, so maybe in this case a custom constraint is the solution.
  • Louis Rhys
    Louis Rhys over 11 years
    yes indeed that was I suspect. Any idea how to create the custom constraint or custom assertion?
  • Louis Rhys
    Louis Rhys over 11 years
    actually, I don't want to assert the order.. Any idea about how to write the helper method?
  • Anders Gustafsson
    Anders Gustafsson over 11 years
    @LouisRhys I have added example code where the order of the objects in the two collections does not matter.
  • AlanT
    AlanT over 11 years
    Using Any() as above will give a problem if the lists can of different lengths. If the expected is a sub-set of the actual then the the test will pass. e.g expected = {A, B}, actual = {A,C,B} {A,B}.Except({A,B,C} = {} To allow for differing lengths one can add a check on counts or run the except in both directions.
  • Anders Gustafsson
    Anders Gustafsson over 11 years
    @AlanT You are absolutely right, sorry for the oversight. I have updated the answer accordingly.
  • Louis Rhys
    Louis Rhys over 11 years
    @AlanT and Anders will there still be problem with {A, A, B} and {A, B, B}? But I think in my case I won't have such comparison
  • AlanT
    AlanT over 11 years
    @Louis Rhys There will be a problem if there are repeated items in either the actual or expected. The set operations used do not allow for multiples of a given item. If repeats can happen then it is possible to compare the lists using 'lhsCount == rhsCount && lhs.Intersect(rhs, equalityComparer).Count() == lhsCount;'
  • Anders Gustafsson
    Anders Gustafsson over 11 years
    Louis, good point about multiple equivalent objects. I have now reworked my answer from scratch taking this into account. Louis, good comment again. But if I am not entirely mistaken, "your" Intersect expression would not react correctly to the example Louis gave ({A, A, B} and {A, B, B}), right? The Intersect would find matches for all its three objects in the second collection and thus return full length, even though the number of A:s and B:s are different in the two collections, right?
  • Gen.L
    Gen.L almost 5 years
    if the Equals() method is override for Foo class already, can we use .AreEqual(ICollection, ICollection) Directly?