4

What is the best way to compare two similar objects?

Given FlintlockDTO and Flintlock:

public class FlintlockDTO
{
  public string GName { get; set; }

  public string SharedPropertyName { get; set; }

  ...
}

and

public class Flintlock
{
  public Flintlock(FlintlockDTO inflator)
  {
    this.GoodName = inflator.GName;
    this.SharedPropertyName = inflator.SharedPropertyName;
    ...
  }

  public string GoodName { get; private set; }

  public string SharedPropertyName { get; private set; }
  ...
}

Where both classes share N properties (e.g. SharedPropertyName), but differ on M properties that are equivalent, but named differently (e.g. GoodName \ GName.)

Tools such as fluentassert nearly do this, if the property names matched, to my understanding this would work:

flintlockDto.ShouldBeEquivalentTo(flintlock);

Is there a way to do this neatly in fluentassert or any other tool?

Ideally,

flintlockDto.IsTheSameAs(flintlock).WhenMapping("GName","GoodName");

4 Answers 4

2

I decided to elaborate more about Likeness mentioned by StriplingWarrior. It's available as a nuget package.

Here is an example:

using NUnit.Framework;
using Ploeh.SemanticComparison;
using Ploeh.SemanticComparison.Fluent;

namespace Tests
{
    [TestFixture]
    class Tests2
    {
        [Test]
        public void ObjectsShuldEqual()
        {
            var flintlockDto = new FlintlockDTO()
            {
                GName = "name",
                AdditionalProperty = "whatever",
                SharedPropertyName = "prop name"
            };
            var flintlock = new Flintlock(flintlockDto);

            Likeness<Flintlock, FlintlockDTO> flintFlockDtoLikeness = flintlock
                .AsSource().OfLikeness<FlintlockDTO>()
                .With(dto => dto.GName).EqualsWhen((flintlock1, dto) => flintlock1.GoodName == dto.GName) // you can write an extension method to encapsulate it
                .Without(dto => dto.AdditionalProperty);

            // assert
            flintFlockDtoLikeness.ShouldEqual(flintlockDto);
        }
    }

    public class FlintlockDTO
    {
        public string GName { get; set; }

        public string SharedPropertyName { get; set; }

        public string AdditionalProperty { get; set; }
    }

    public class Flintlock
    {
        public Flintlock(FlintlockDTO inflator)
        {
            this.GoodName = inflator.GName;
            this.SharedPropertyName = inflator.SharedPropertyName;
        }

        public string GoodName { get; private set; }

        public string SharedPropertyName { get; private set; }
    }
}

As you can see:

  • It performs auto comparison on properties with the same names
  • You can specify if properties of different names should match (this one is actually pretty ugly out of the box, but you can write an extension method to encapsulate it)
  • You can specify if a property should not be compared
Sign up to request clarification or add additional context in comments.

Comments

2

One strategy that I sometimes use when there are specific properties I want to compare together is to leverage anonymous types, like so:

Assert.AreEqual(
    new{flintlockDto.GoodName, flintlockDto.SharedPropertyName},
    new{GoodName = flintlock.GName, flintlock.SharedPropertyName});

This doesn't rely on any particular testing framework. It leverages the auto-generated Equals() methods for anonymous types, and in the case of failure the auto-generated ToString() method gives you a full description of what the two objects looked like, which makes it easy to figure out what went wrong.

You may also want to look into Mark Seemann's Likeness type:

How do we resolve this conundrum without introducing equality pollution? AutoFixture offers one option in the form of the generic Likeness class. This class offers convention-based test-specific equality mapping from TSource to TDestination and overriding the Equals method.

... It's possible to customize the comparison to override the behavior for certain properties...

Comments

1

You can write an extension class/method inside your test project and achieve your desired result, and you won't pollute your production codebase with unnecessary logic.

static class Extensions
{
  public static bool IsEqualTo(this FlintlockDTO expected, Flintlock actual) 
  {
    return expected.GName == actual.GoodName && expected.SharedPropertyName == actual.SharedPropertyName;
  }
}

In your test, then you'll be able to run this:

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

and the test logic won't be available in your production codebase.

Comments

0

It might be good to have FlintlockDTO implement IEquatable<Flintlock> as follows:

public override Equals(Flintlock other)
{
    return GName == other.GoodName; // modify as necessary (case, culture, etc.)
}

(And don't forget to override and properly implement GetHashCode.)

Then, you can just check if

flintlockDto.Equals(flintlock)

in the unit testing framework of your choice.

5 Comments

I would feel really uncomfortable giving people the impression that two objects of different types are "Equal". Also, this signature doesn't override the Equals(object) signature, so it wouldn't actually work with most test frameworks' AreEqual() assertions. And, to maintain parity, would you also need to implement the same equality check in the Flintlock class, going the other direction?
@StriplingWarrior: But that's exactly what the OP wants to test for. And even though they are different types, they represent the same information - one is just a Data Transfer Object for the other.
Yes, but changing the Equals() and GetHashCode() behavior of two separate production classes seems like a lot of Test-Induced Damage to accommodate a basic unit test.
Overriding Equals in this case would only affect the comparison from FlintlockDTO to Flintlock, which is what the OP is trying to assert in the tests. It is also more maintainable - no hard-coded property names as strings and no individual property checking in the test methods.
Allowing your tests to impact production code in this way is an anti-pattern known as "Equality Pollution". There are other, more composable, ways to keep this check maintainable, while still keeping your equality check test-specific

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.