How to assert that two list contains elements with the same public properties in NUnit?
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.
Comments
-
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 over 11 yearsYou 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 over 11 yearsThen 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 over 11 yearshow to "JSON serialize" in a simple way?
-
Louis Rhys over 11 yearsis that different from CollectionAssert.AreEquivalent? anyway both does not work, returning similar exception about objects not being equal
-
Erwin over 11 yearsI 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 over 11 yearsyes indeed that was I suspect. Any idea how to create the custom constraint or custom assertion?
-
Louis Rhys over 11 yearsactually, I don't want to assert the order.. Any idea about how to write the helper method?
-
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 over 11 yearsUsing 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 over 11 years@AlanT You are absolutely right, sorry for the oversight. I have updated the answer accordingly.
-
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 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 over 11 yearsLouis, 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? TheIntersect
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 almost 5 yearsif the
Equals()
method is override forFoo
class already, can we use.AreEqual(ICollection, ICollection)
Directly?