xUnit for unit testing controllers with AutoMapper returns “Mapper already initialized” error

on

I am fairly new to unit testing with xUnit, so some of my comments may not be best practices, but I did come across an interesting situation when trying to develop unit tests for my controllers.

The problem

The initial problem was that I had a situation where, when I ran any of my unit tests individually, they would work fine. However, when I chose “Run all”, some of them (not all of them, and not even the same ones each time!) would fail due to the “Mapper already initialized” error.

The question and my setup can easily be found described in the Stack Overflow question I submitted. Here I am going to talk a little bit more on what I found while solving this issue.

The code to follow along is available in my GitHub repository.

Static API setup of AutoMapper

For this project we were using ASP.NET Core 2.2, AutoMapper 8.0.0. and xUnit 2.4.1. However, this should not be relevant, this is not an error but expected behavior and I will try and explain what is happening and why.

I had the AutoMapper set up using the static API. In a class called MapperConfig.cs I had my mappings defined:

public static class MapperConfig
{
    public static void RegisterMaps()
    {
        AutoMapper.Mapper.Initialize(config =>
        {
            config.CreateMap<Item, ItemDTO>();    
            config.CreateMap<ItemDTO, Item>();
        });
    }
}

and I’d call the RegisterMaps method in the Configure method of my Startup.cs.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    MapperConfig.RegisterMaps();
    // ...
}

The controllers were thin and their job was to get the data from a service, transform it into data transfer objects and send it back via the API. For the transformation between data and DTOs the static Mapper class was being used.

[Route("api/[controller]")]
[ApiController]
public class ItemsStaticController : ControllerBase
{
    private readonly IItemService _itemService;

    public ItemsStaticController(IItemService itemService)
    {
        _itemService = itemService;
    }

    // GET: api/Items
    [HttpGet]
    [ProducesResponseType(typeof(List<ItemDTO>), 200)]
    public async Task<IActionResult> Get()
    {
        var items = await _itemService.Get();
        var itemDTOs = Mapper.Map<List<ItemDTO>>(items);
        return Ok(itemDTOs);
    }
}

For writing unit tests I was using xUnit and the Moq framework. Normally I do not organize my test classes like in the GitHub repository. But here, for demonstration purposes, I put each test into a single class, just so I can show what happens when multiple initializations of AutoMapper happen.

Tests looked like this:

public class UnitTest1
{
    private ItemsStaticController controller;

    public UnitTest1()
    {
        AutoMapper.Mapper.Reset();
        MapperConfig.Initialize();

        var itemServiceMock = new Mock<IItemService>();
        itemServiceMock.Setup(service => service.Get()).ReturnsAsync(
            new List<Item>
            {
                new Item
                {
                    Id = Guid.NewGuid(),
                    Name = "Juice",
                    Description = "Orange juice with 30% of orange.",
                    Price = 6.99
                },
                new Item
                {
                    Id = Guid.NewGuid(),
                    Name = "Chocolate 80g",
                    Description = "Milk chocolate with pieces of almond and nuts.",
                    Price = 8.99
                },
                new Item
                {
                    Id = Guid.NewGuid(),
                    Name = "Bread",
                    Description = "1 loaf of bread, white",
                    Price = 4.99
                }
            }
        );

        controller = new ItemsStaticController(itemServiceMock.Object);
    }

    [Fact]
    public async void GetItemsReturnsOk()
    {
        // Arrange
 
        // Act
        var result = await controller.Get();

        // Assert
        Assert.IsType<OkObjectResult>(result);
    }

All tests looked simple enough. However, when I chose the Run all tests option, I was getting an error:

AutoMapper - Mapper already initialized error

In several places online I found that, to ensure the AutoMapper is not initialized twice, we just reset it before initialization. So I just added an AutoMapper Reset command above the initialization:

AutoMapper.Mapper.Reset();
MapperConfig.Initialize();

But this did not help either, still the same error occurred. What was even more annoying is that, if I ran all the tests one by one, they all passed, and if I clicked “Run all” more than once, each time a different test (or tests!) would fail.

What was happening here?

Race condition with a shared resource

As Nkosi pointed out in one of the answers to my StackOverflow question, I had a situation of a race condition with a shared resource.

When you have multiple unit tests, you want them to run in parallel, so that you spend less time overall. How xUnit tests are run in parallel is explained in more detail here.

But if more than one of these constructors is called in parallel, and each of them is calling the Initialize method of a static AutoMapper instance. So, the static AutoMapper instance is the shared resource and all test classes are racing to initialize it. Not even resetting it helps, because it can happen that between the resets more than one Initialize is called.

This is why the exception of “Mapper already initialized” was thrown.

Possible solution: spin up a web server to be used in integration testing

One of the ideas Dmitry Pavlov suggested in responding to my StackOverflow question was to spin up a test server (like you would when doing integration testing). Then you can have a specific configuration for that Test Server where you can ensure that this initialization is done only once.

However, I did not like the idea of spinning up a full test server just to run unit tests. For integration tests it would make perfect sense. However, unit tests should focus only on a single unit that is being tested and test only its functionality, regardless of the whole system it is a part of. Therefore I did not choose this option.

Instance based API of AutoMapper

AutoMapper has the Instance API option as well. The instance API option allows you to create a specific instance of the mapper and then inject it inside any controller (or other entity) where you might need it.

In order to adapt the existing code to use the Instance based API in the ConfigureServices method I needed to add:

// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
    mc.AddProfile(new MyMappingProfile());
});

IMapper mapper = mappingConfig.CreateMapper();
services.AddSingleton(mapper);

With MyMappingProfile class looking like:

public class MyMappingProfile: Profile
{
    public MyMappingProfile()
    {
        CreateMap<Item, ItemDTO>();
        CreateMap<ItemDTO, Item>();
    }
}

And the controller needed to be reworked to take the AutoMapper instance through dependency injection:

[Route("api/[controller]")]
[ApiController]
public class ItemsInstanceController : ControllerBase
{
    private readonly IItemService _itemService;
    private readonly IMapper _mapper;

    public ItemsInstanceController(IItemService itemService, IMapper mapper)
    {
        _itemService = itemService;
        _mapper = mapper;
    }

    // GET: api/Items
    [HttpGet]
    [ProducesResponseType(typeof(List<ItemDTO>), 200)]
    public async Task<IActionResult> Get()
    {
        var items = await _itemService.Get();
        var itemDTOs = _mapper.Map<List<ItemDTO>>(items);   // version with singleton AutoMapper
        return Ok(itemDTOs);
    }
}

Note the line services.AddSingleton(mapper). The AutoMapper is added as a singleton, meaning that the same instance is always used everywhere. One would think that this would solve the problem for the unit tests as well.

However, when you run unit tests, the configuration code from Startup.cs is not run. This means that the AutoMapper will not be configured as a singleton for the unit tests automatically.

AutoMapper as a singleton in unit tests

The goal is to use AutoMapper as a singleton in unit tests. In order to do this, we will add a helper class:

public class AutomapperSingleton
{
    private static IMapper _mapper;
    public static IMapper Mapper {  get
    {
        if (_mapper == null )
        {
            // Auto Mapper Configurations
            var mappingConfig = new MapperConfiguration(mc =>
            {
                 mc.AddProfile(new ExamsMappingProfile());
            });
            IMapper mapper = mappingConfig.CreateMapper();
            _mapper = mapper;
        }
        return _mapper;
    }
}

This is a basic implementation of the singleton pattern which will enable our tests to use the same instance of AutoMapper and avoid the error of reinitializing it every time a test is run.

The way to use this in the test classes is:

controller = new ItemsInstanceController(itemServiceMock.Object, AutomapperSingleton.Mapper);

This will avoid multiple AutoMapper initialization attempts.

Conclusion

From my experience, instance based AutoMapper usage is more natural with the DI principles and makes code easier to test. Not saying that the static approach is wrong, and there probably is a way to get around the problem I had.

However, as I use dependency injection to inject pretty much anything I can, to make code as clean and testable as possible, I do not see why I would use a different approach with AutoMapper.

I would love to hear other opinions in the discussion below.

9 thoughts on “xUnit for unit testing controllers with AutoMapper returns “Mapper already initialized” error

  1. Thanks, I now worked on the project, where using automapper and I cannot refactor for a run all tests in explorer(

  2. Great post! Exactly what I was looking for. Initially, I fell into the trap of creating a collection fixture but that would have meant all the tests run sequentially. Not anymore!

Leave a Reply

Your email address will not be published. Required fields are marked *

You are currently offline