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

拥有回忆 提交于 2019-11-28 07:41:15

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

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.

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.

Ruben Bartelink

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

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!