Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Applying [AutoFixture] SemanticComparison OfLikeness to sequences / collections / arrays / IEnumerable

We have written a test which looks like the following. This test requires that we have created en Equal-overload for the CodeTableItem-class:

ICollection<CodeTableItem> expectedValutaList = new List<CodeTableItem>();
expectedValutaList.Add(new CodeTableItem("DKK", "DKK"));
expectedValutaList.Add(new CodeTableItem("EUR", "EUR"));
RepoDac target = new RepoDac(); 

var actual = target.GetValutaKd();

CollectionAssert.AreEqual(expectedValutaList.ToList(),actual.ToList());

The test works fine, but has the unfortunate dependency to the Equality-function, meaning if I extend the CodeTableItem-class with one more field, and forgets to extend the Equals-function, the unit test still runs green, although we do not test for all fields. We want to avoid this Equality pollution (see Test Specific Equality), which has been written only to conform to the test.

We have tried using OfLikeness, and have rewritten the test in this way:

ICollection<CodeTableItem> expectedValutaList = new List<CodeTableItem>();
expectedValutaList.Add(new CodeTableItem("DKK", "DKK"));
expectedValutaList.Add(new CodeTableItem("EUR", "EUR"));
var expectedValutaListWithLikeness = 
          expectedValutaList.AsSource().OfLikeness<List<CodeTableItem>>();

RepoDac target = new RepoDac(); 
ICollection<CodeTableItem> actual;

actual = target.GetValutaKd();

expectedValutaListWithLikeness.ShouldEqual(actual.ToList());

But the test fails because the Capacity is not equal. I have written code that runs through reflection many times, and typically ended up implementing overloads for ignoring fields. Is there a way to ignore certain fields with the OfLikeness or ShouldEqual? Or is there some other way of solving this issue?

like image 796
Tom Kise Avatar asked Jul 30 '12 09:07

Tom Kise


3 Answers

Why you don't want to do it like that

I don't think creating a Likeness from any List<T> does what you want it to do. As I understand, you want to compare the contents of two lists. That's not the same as comparing two lists...

Consider what Likeness does: it compares property values. What are the properties of List<T>?

They are

  • Capacity
  • Count

As Nikos Baxevanis points out in his answer, you can use the Without method to ignore the value of the Capacity property, but that means that only the Count property remains.

In other words, if you did that, this:

expectedValutaListWithLikeness.ShouldEqual(actual.ToList());

would be functionally equivalent to this:

Assert.AreEqual(expected.Count, actual.Count)

In other words, the lists could have totally different data, but the test would still pass if only each list has the same amount of elements. That's probably not what you want...

What you should do

You can use Likeness to compare each element against each other. Something like this should work:

var expectedValutaList = new List<CodeTableItem>();
expectedValutaList.Add(new CodeTableItem("DKK", "DKK"));
expectedValutaList.Add(new CodeTableItem("EUR", "EUR"));

var expectedValutaListWithLikeness = from cti in expectedValutaList
                                     select cti
                                         .AsSource()
                                         .OfLikeness<CodeTableItem>();

var target = new RepoDac(); 

var actual = target.GetValutaKd();

Assert.IsTrue(expectedValutaListWithLikeness.Cast<object>().SequenceEqual(
    actual.Cast<object>()));

You may also be able to use CollectionAssert for the assertion, but it's been so many years since I last used MSTest that I can't remember the quirks of that method...

like image 162
Mark Seemann Avatar answered Oct 26 '22 12:10

Mark Seemann


Just add the .Without(x => x.Capacity) and the Likeness instance will ignore the Capacity property when comparing values.

var expectedValutaListWithLikeness = 
      expectedValutaList.AsSource().OfLikeness<List<CodeTableItem>>()
      .Without(x => x.Capacity);

Update:

As Mark Seemann points out in his answer, what you probably want is to compare each element against each other. Here is a slightly different way that allows you to perform very flexible comparisons.

Assuming that the RepoDac class returns something like:

public class RepoDac
{
    public ICollection<CodeTableItem> GetValutaKd()
    {
        return new[]
        {
            new CodeTableItem("DKK", "DKK"),
            new CodeTableItem("EUR", "EUR")
        };
    }
}

For each instance on the expectedValutaList you can create a dynamic proxy that overrides Equals using Likeness:

var object1 = new CodeTableItem("DKK", "DKK1")
    .AsSource().OfLikeness<CodeTableItem>()
    .Without(x => x.Property2)
    .CreateProxy();

var object2 = new CodeTableItem("EUR2", "EUR")
    .AsSource().OfLikeness<CodeTableItem>()
    .Without(x => x.Property1)
    .CreateProxy();

Notice how the object1 and object2 have even different dynamically generated Equals. (The first ignores Property2 while the second ignores Property1.)

The test below passes:

var expected = new List<CodeTableItem>();
expected.Add(object1);
expected.Add(object2);

var target = new RepoDac();
var actual = target.GetValutaKd();

Assert.IsTrue(expected.SequenceEqual(actual));

Note:

It is required to start with the expected instance which contains the dynamically generated proxies (overriding Equals).

You may find more information on this feature here.

like image 32
Nikos Baxevanis Avatar answered Oct 26 '22 12:10

Nikos Baxevanis


I wanted to make this explicit for others having this problem - using Ruben's second code example, where you want to compare both Calendar and Calendar.Holidays, while customizing the comparison of both:

var expectedCalendar = newCalendar.AsSource()
   .OfLikeness<Calendar>()
   .Without(c=>c.Id) //guid, will never be equal
   .With(c=>c.Holidays).EqualsWhen((source, dest) => 
      source.Holidays.SequenceLike(dest.Holidays, holiday => 
          holiday.Without(h=>h.SecondsUntil) //changes every second
   ));

In this example, you first setup properties to exclude, etc on the Calendar object. Then you give a custom EqualsWith implementation for handling the Holidays collection. The holiday=> lambda then allows you to customize the child comparison, just like the parent. You can continue nesting so long as you enjoy lots of parenthesis.

like image 3
russbishop Avatar answered Oct 26 '22 13:10

russbishop