AutoMapper Madness - Unit Testing Your Maps

August 16, 2022#Software Development
Article
Author image.

Scott DePouw, Senior Consultant

This is the second part of my blog post series on AutoMapper. Check out the first part here!

AutoMapper: A Very Testable Tool

One of the many reasons I use AutoMapper frequently, utility aside, is how unit testable it is. All operations occur from an injected IMapper instance and it’s easy to implement your desired mappings in a pure Test-Driven Development fashion. I can create a unit test specifying what I want out of a mapping, observe its failure (for the right reason), make it pass, then refactor application or test code after all lights read green. This post delves into how I unit test AutoMapper and how I structure my tests to reduce code duplication as much as possible.

What I Don’t Test

Before diving into the unit tests I do write, I wanted to mention the sort of unit tests I skip for AutoMapper. I won’t, generally-speaking, write unit tests that assures Foo.Prop maps to Bar.Prop, or any other mappings that AutoMapper will do without any configuration beyond CreateMap<Foo, Bar>(). This is out-of-the-box functionality and thoroughly unit tested within AutoMapper itself, and my test files would be a mile long if I meticulously tested every single property in this fashion.

There is an exception to this rule of mine, though, which I’ll talk about in a bit. But for now…

Let’s Write Some Unit Tests!

There is a lot you can test within AutoMapper beyond its default functionality, starting with the most basic: Asserting that AutoMapper does not blow up when attempting to map two classes you expect to work together.

using AutoMapper;
using MyApp.Core;
using MyApp.Core.AutoMapper;
using Xunit;

namespace MyApp.UnitTests.AutoMapper;

public class MapFooToBarShould
{
    private readonly IMapper _mapper = AutoMapperConfig.Initialize();
    
    [Fact]
    public void MapWithoutThrowing()
    {
        _mapper.Map<Bar>(new Foo());
    }
}

Pretty straightforward example here. AutoMapper will throw an AutoMapperMappingException with the message that begins with “Missing type map configuration or unsupported mapping.” The rest of the message details what exactly went wrong, and is generally pretty thorough, which is great for debugging purposes. In the case of our test, we just call the mapping, and the test will pass provided nothing is thrown. (XUnit, unlike NUnit, lacks Assert.DoesNotThrow, since their philosophy is that you shouldn’t explicitly assert that, and the test will pass by virtue of no expected exceptions being thrown.)

This is a perfect way to kick off a pure TDD approach to unit testing AutoMapper: Obviously this test will throw when you haven’t done anything yet, and this prompts you to add this mapping to your Profile class (or to create the Profile class in the first place).

using AutoMapper;
using MyApp.Core;

namespace MyApp.Core.AutoMapper;

public class MyAppProfile : Profile
{
    public MyAppProfile()
    {
        CreateMap<Foo, Bar>();
    }
}

The test then passes, then you can move on to the real meat and potatoes of AutoMapper unit testing.

Configurations In Profile Classes

Now that AutoMapper will perform a mapping from Foo to Bar, we can start adding non-default mappings that aren’t picked up by AutoMapper by default.

namespace MyApp.Core;

public class Foo
{
    public string Prop { get; set; } = string.Empty;
    public string OtherProp { get; set; } = string.Empty;
    public int Number { get; set; }
}

public class Bar
{
    public string Prop { get; set; } = string.Empty;
    public string OtherProp { get; set; } = string.Empty;
    public int OtherNumber { get; set; }
}

Here, Prop and OtherProp will be automatically mapped with no intervention, so we skip those in terms of testing. However, we want Number to map to OtherNumber. So, let’s write a test covering that scenario.

[Fact]
public void MapNumberToOtherNumber()
{
    Foo foo = new() { Number = 42 };
    
    Bar bar = _mapper.Map<Bar>(foo);
    
    Assert.Equals(42, bar.OtherNumber);
}

Once it fails we make the test pass by configuring our mapping:

CreateMap<Foo, Bar>()
    .ForMember(dest => dest.OtherNumber, opt => opt.MapFrom(src => src.Number));

If we had more mismatching properties we would iterate over them until we have full test coverage. If you find yourself adding dozens of these tests, then it’s possible that AutoMapper isn’t the correct tool in this situation, as very little automatic mapping is occurring and you’re just manually mapping things anyway. At this point, it may be wiser to create a Factory class that does this object creation, but it all comes down to your specific scenario!

AssertConfigurationIsValid

Our first AutoMapper test, making sure mapping from Foo to Bar doesn’t throw, is a solid first test. However, it doesn’t strictly check that created objects are fully mapped, with all properties accounted for. AutoMapper has a convenient method AssertConfigurationIsValid. This method will check every mapping you’ve created, assuring that destination objects are fully-hydrated when a mapping occurs.

Using our example of Foo and Bar above, imagine if we never specified a mapping from Number to OtherNumber. If we write a test calling AssertConfigurationIsValid, a unit test will now cover the fact that we missed a property mapping! In my code, I tend to frame this unit test around testing AutoMapperConfig, the class made in my previous post that provides an Initialize() method for IMapper.

using AutoMapper;
using MyApp.Core.AutoMapper;
using Xunit;

namespace MyApp.UnitTests.AutoMapper;

public class AutoMapperConfigInitializeShould
{
    [Fact]
    public void CreateValidMappingConfiguration()
    {
        IMapper mapper = AutoMapperConfig.Initialize();

        mapper.ConfigurationProvider.AssertConfigurationIsValid();
    }
}

When running this test we get an AutoMapperConfigurationException, followed by a list of all classes and properties with missed mappings.

AutoMapper.AutoMapperConfigurationException

Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
=================================================================================
Foo -> Bar (Destination member list)
MyApp.Core.Foo -> MyApp.Core.Bar (Destination member list)

Unmapped properties:
OtherNumber

In truth, I add this unit test the instant I install AutoMapper and create my AutoMapperConfig class. I don’t always fix it immediately (when I add a new mapping, I leave it to that class to pure-TDD things), but this is a wonderful catch-all, to make sure you’re mapping (or ignoring) every property.

An Exception to What I Don’t Test: Regressions

Let’s say a request comes down the pipeline to change Foo or Bar, adding a new property. Since we’re in control of the naming, we decide to make things easy by calling both properties the same thing: NewProperty. Now, if this property existed before introducing AutoMapper, I would not do this. Since this is a new addition, I typically add a regression test in the form of a unit test, confirming that this default-behavior mapping occurs. This lets me continue pure TDD by writing the test, watching it fail (because the properties don’t exist), then adding the properties and re-running all my tests. This new test (and the AssertConfigurationIsValid test from earlier) verifies that the new property is in place and is mapping.

Base Classes to Reduce Redundancy

I don’t like writing duplicate code in testing any more than I like it in application code, I don’t want to write that same first test for every new mapping I’ll end up needing. Wouldn’t it be convenient if we could just make a test class inherit from a base class, that automatically sets up this test for us?

AutoMapperTestBase

This abstract base class exists in every project that I test AutoMapper in. All it does is get my AutoMapper instance, then try to map an instance of TSource to an instance of TDestination. AutoMapper will throw if this mapping isn’t defined.

using AutoMapper;
using MyApp.Core.AutoMapper;
using Xunit;

namespace MyApp.UnitTests.AutoMapper;

public abstract class AutoMapperTestBase<TSource, TDestination>
    where TSource : new()
{
    protected readonly IMapper _mapper = AutoMapperConfig.Initialize();

    [Fact]
    public void MapWithoutThrowing()
    {
        _mapper.Map<TDestination>(new TSource());
    }
}

Now, whenever I desire a new mapping, I can kick off the TDD process with just a couple lines of code.

using MyApp.Core;

namespace MyApp.UnitTests.AutoMapper;

public class MapFooToBarShould : AutoMapperTestBase<Foo, Bar> { }

This will, of course, fail until I create the map between Foo and Bar. I’ll extend this test file only if this mapping has any behavior outside the default behavior provided by AutoMapper. Short and sweet!

What if My Source Class Lacks an Empty Constructor?

You might have noticed the where TSource: new() type constraint in AutoMapperTestBase. This is used so deriving classes don’t have to make an instance themselves. An class to map that lacks an empty constructor is very rare in my experience, but when it happens, I can’t use AutoMapperTestBase in its current form. There are options you can take to work around this:

  • Add empty constructor to the class in question
  • Do the base class work in your test class as a one-off
  • Change AutoMapperTestBase to delegate object creation in the inheriting test class

AutoMapper Base Test Class Potential

Depending on your needs, you can create more base classes similar to the above. (There is such a thing as going overboard here, though, so do so with caution! Folks don’t like navigating a sea of hierarchy!) A common one I’ll add if it becomes needed in an application is one that tests bidirectional mapping, i.e. Foo should map to Bar and Bar should map to Foo. This class, something like BidirectionarlAutoMapperTestBase, would inherit from AutoMapperTestBase and do the same thing that does, in reverse:

public abstract class BidirectionalAutoMapperTestBase<TSource, TDestination> : AutoMapperTestBase<TSource, TDestination>
    where TSource : new()
    where TDestination : new()
{
    [Fact]
    public void MapOtherDirectionWithoutThrowing()
    {
        _mapper.Map<TSource>(new TDestination());
    }
}

Any class that derives from this one now gets two tests for free instead of one, asserting a mapping can happen in either direction.

Wrap-Up

This post discussed how I go about unit testing AutoMapper in a pure TDD fashion. We covered unit testing our own mappings, using built-in validators AutoMapper provides, and went over things we don’t write tests for. In my next post we’ll be extending unit testing a bit further, as we’ll be going over a specific use-case for AutoMapper involving self-mappings, and how to test that AutoMapper behaves how we expect. See you then!


Copyright © 2024 NimblePros - All Rights Reserved