Resemblance and Likeness by Mark Seemann
In a previous post I described the Resemblance idiom. In this post I present a class that can be used to automate the process of creating a Resemblance.
The Resemblance idiom enables you to write readable unit tests, but the disadvantage is that you will have to write (and maintain) quite a few test-specific Resemblance classes. If you're a Coding Neat Freak like me, you'll also have to override GetHashCode in every Resemblance, just because you're overriding Equals. If you're still looking for ways to torment yourself, you'll realize that the typical Resemblance implementation of Equals contains branching logic, so ideally, it should be unit tested too. (Yes, I do occasionally unit test my unit testing helper classes. AutoFixture, which is essentially one big unit test helper, is currently covered by some three thousand tests.)
In that light, wouldn't it be nice if you were able to automate the process of defining Resemblance classes? With the Likeness class, you can. Likeness is currently bundled with AutoFixture, so you get it if you install the AutoFixture NuGet package or if you download the AutoFixture zip file from its CodePlex site. The Likeness class can be found in an assembly named Ploeh.SemanticComparison. In the future, I plan on packaging this as a separate NuGet package, but currently, it's included with AutoFixture.
Likeness #
Before I show you how to turn a Likeness into a Resemblance, I think a very short introduction to Likeness is in order. A couple of years ago I introduced Likeness on my blog, but I think a re-introduction is in order. In the context of the unit test from the previous blog post, instead of manually writing a test like this:
[Theory, AutoWebData] public void PostSendsOnChannel( [Frozen]Mock<IChannel<RequestReservationCommand>> channelMock, BookingController sut, BookingViewModel model) { sut.Post(model); var expected = model.MakeReservation(); channelMock.Verify(c => c.Send(It.Is<RequestReservationCommand>(cmd => cmd.Date == expected.Date && cmd.Email == expected.Email && cmd.Name == expected.Name && cmd.Quantity == expected.Quantity))); }
Likeness will do it for you, using Reflection to match properties according to name and type. By default, Likeness compares all properties of the destination instance. This will not work in this test, because the RequestReservationCommand also includes an Id property which is a Guid which is never going to be the same across different instances:
public MakeReservationCommand( DateTime date, string email, string name, int quantity) { this.date = date; this.email = email; this.name = name; this.quantity = quantity; this.id = Guid.NewGuid(); }
Notice that the above test exludes the Id property. It's not very clear that this is going on, so a Test Reader may think that this is an accidental omission. With Likeness, this becomes explicit:
[Theory, AutoWebData] public void PostSendsOnChannel( [Frozen]Mock<IChannel<MakeReservationCommand>> channelMock, BookingController sut, BookingViewModel model) { sut.Post(model); var expected = model.MakeReservation() .AsSource().OfLikeness<MakeReservationCommand>() .Without(d => d.Id); channelMock.Verify(c => c.Send(It.Is<MakeReservationCommand>(x => expected.Equals(x)))); }
Notice how the Without method explicitly states that the Id property should be excluded from the comparison. By convention, all other properties on the target type (MakeReservationCommand, which in this case is the same as the source type) must be equal to each other.
The AsSource().OfLikeness extension method chain creates an instance of Likeness<MakeReservationCommand, MakeReservationCommand>, which isn't the same as an instance of MakeReservationCommand. Thus, you are still forced to use the slightly awkward It.Is syntax to express what equality means:
channelMock.Verify(c => c.Send(It.Is<MakeReservationCommand>(x => expected.Equals(x))));
If a Likeness could be used as a Resemblance, the test would be more readable.
Likeness as Resemblance #
The definition of the Likeness class is this:
public class Likeness<TSource, TDestination> : IEquatable<TDestination>
While it implements IEquatable<TDestination> it isn't a TDestination. That's the reason for the It.Is syntax above.
Thanks to awesome work by Nikos Baxevanis, a Likeness instance can create a dynamic proxy of TDestination, as long as TDestination is non-sealed and doesn't seal the Equals method. The method is called CreateProxy:
public TDestination CreateProxy();
This effectively turns the Likeness into a Resemblance by dynamically emitting a derived class that overrides Equals in the way that the Likeness instance (re)defines equality. With that, you can rewrite the unit test so that it's both readable and maintainable:
[Theory, AutoWebData] public void PostSendsOnChannel( [Frozen]Mock<IChannel<RequestReservationCommand>> channelMock, BookingController sut, BookingViewModel model) { sut.Post(model); var expected = model.MakeReservation() .AsSource().OfLikeness<RequestReservationCommand>() .Without(d => d.Id) .CreateProxy(); channelMock.Verify(c => c.Send(expected)); }
The only difference in the definition of the expected variable is the additional call to CreateProxy. This changes the type of expected so that it's no longer Likeness<MakeReservationCommand, MakeReservationCommand>, but rather MakeReservationCommand (actually, a dynamically created sub-type of MakeReservationCommand). This enables you to write a readable assertion:
channelMock.Verify(c => c.Send(expected));
This is exactly the same assertion as the assertion used with the Resemblance idiom described in the previous post. Thus, Likeness has now been turned into a Resemblance, and you no longer have to manually create and maintain concrete Resemblance classes.
Comments
Once again, you guys have done it. I've used foo.AsSource().OfLikeness() before, but the CreateProxy trick solved the exact problem I was having.
I'm continually impressed with the power you've packed into this relatively small tool (AutoFixture).