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

5

14

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?

Siena answered 30/7, 2012 at 9:33 Comment(0)
M
12

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...

Martinmas answered 30/7, 2012 at 11:56 Comment(0)
M
8

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.

Multure answered 30/7, 2012 at 10:7 Comment(0)
D
3

The following answer emanated from me asking myself a duplicate of this question, see below

You could use a SequenceLike operation which alludes to LINQ's SequenceEqual operator.

This allows one to write:-

[Theory, AutoData]
public void ShouldMap(  Dto inputDto )
{
    var mapped = inputDto.ToModel();

    inputDto.AsSource().OfLikeness<Model>()
        .Without( x => x.IgnorableProperty )
        .With( x => x.Tags ).EqualsWhen( ( dto, model ) => 
            model.Tags.SequenceLike( dto.Tags ) )
        .ShouldEqual( mapped );
}

Shiny short implementation of all-in-one helper based on @Mark Seemann's answer thanks to prompting from @Nikos Baxevanis:-

static class LikenessSequenceExtensions
{
    public static bool SequenceLike<T, TSource>( this IEnumerable<T> that, IEnumerable<TSource> source )
    {
        return SequenceLike<T, TSource>( that, source, x => x );
    }

    public static bool SequenceLike<T, TSource>( this IEnumerable<T> that, IEnumerable<TSource> source, Func<Likeness<TSource, T>, IEquatable<T>> customizeLikeness )
    {
        return source.Select( x => customizeLikeness( x.AsSource().OfLikeness<T>() ) ).SequenceEqual( that.Cast<object>() );
    }
}

My original implementation:

static class LikenessSequenceExtensions0
{
    public static bool SequenceLike0<T, TSource>( this T[] that, TSource[] source )
    {
        return source.SequenceLike0( that, likeness => likeness );
    }

    public static bool SequenceLike0<T, TSource>( this T[] that, TSource[] source, Func<Likeness<TSource, T>, IEquatable<T>> customizeLikeness )
    {
        return source.SequenceEqual( that, ( x, y ) => customizeLikeness( x.AsSource().OfLikeness<T>() ).Equals( y ) );
    }

    public static bool SequenceEqual<T, TSource>( this T[] that, TSource[] source, Func<T, TSource, bool> equals )
    {
        return that.Length == source.Length && that.Zip( source, Tuple.Create ).All( x => equals( x.Item1, x.Item2 ) );
    }
}

Original duplicate question

I'm looking for the cleanest way to manage Test Specific Equality for arrays/IEnumerable<T>/seq<'T> in my xunit.AutoFixture-based tests.

OOTB (I've lost the reference to where I learned this), Ploeh.SemanticComparison's Likeness versions up to 2.12 works on individual items only.

What is the best way to apply the same techniques to collections of items (ideally OOTB but very open to well-judged suite of extension methods) to facilitate expressing likenesses of items that include embedded objects in a composable manner?

This really is a self-answer so I can stash the helpers and allow a place to put a "you do it this way in V n.n" should Likeness offer sequence support in the future, but it wouldn't be the first time I've been surprised with the subtlety of answer possible from AFflicted AFicionados

Dehydrogenase answered 10/10, 2012 at 12:21 Comment(0)
N
3

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.

Nefertiti answered 18/12, 2012 at 6:1 Comment(1)
+1 Nice. And hey, those friendly embracing parentheses are very comforting :)Dehydrogenase
D
1

I'm not sure if this question is still relevant, but what you are looking for is https://github.com/jmansar/SemanticComparisonExtensions

You can use .WithCollectionInnerLikeness() to do collection comparisons and it'll get you exactly what you want.

Diphosgene answered 8/9, 2017 at 18:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.