4

I'm hoping that somebody can help me. We are currently upgrading AutoMapper from v6 to v9 - we would go to v10 but the inability to create new ResolutionContext impacts our unit testing. That said with v9 we are still having the following issue with unit testing AutoMapper Converters...

A ConverterClass:

public class FooBarConverter :
    ITypeConverter<FooSourceObject, BarDestinationObject>
{
    /// <inheritdoc/>
    public virtual BarDestinationObjectConvert(FooSourceObjectsource, BarDestinationObjectdestination, ResolutionContext context)
    {
        EnsureArg.IsNotNull(source, nameof(source));

        switch (source.Type)
        {
            case SomeType.None:
                return context.Mapper.Map<NoneBarDestinationObject>(source);
            case SomeType.FixedAmount:
                return context.Mapper.Map<FixedBarDestinationObject>(source);
            case SomeType.Percentage:
                return context.Mapper.Map<PercentageBarDestinationObject>(source);
            default:
                throw new ArgumentOutOfRangeException(nameof(source));
        }
    }

Before in AutoMapper 6 we had the following Unit Test (using Xunit):

public class FooBarConverterTests
{
    private readonly FooBarConverter target;

    private readonly Mock<IRuntimeMapper> mockMapper;
    private readonly ResolutionContext resolutionContext;

    public FooBarConverterTests()
    {
        this.mockMapper = new Mock<IRuntimeMapper>();
        this.resolutionContext = new ResolutionContext(null, this.mockMapper.Object);
        this.target = new FooBarConverter();
    }

    [Fact]
    public void FixedAmountFooModel_ConvertsTo_FixedBarDomainModel()
    {
        // Arrange
        var input = new Foo
        {
            Type = SomeType.FixedAmount
        };

        var expected = new DomainModels.FixedBarDestinationObject();

        this.mockMapper.Setup(x => x.Map<FixedBarDestinationObject>(It.IsAny<FooSourceObjectsource>()))
            .Returns(expected);

        // Act
        var actual = this.target.Convert(input, It.IsAny<BarDestinationObjectdestination>(), this.resolutionContext);

        // Assert
        actual.ShouldSatisfyAllConditions(
            () => actual.ShouldNotBeNull(),
            () => actual.ShouldBeAssignableTo<DomainModels.FixedBarDestinationObject>());

        this.mockMapper.Verify(x => x.Map<DomainModels.FixedBarDestinationObject>(input));
    }
}

Essentially, this was working fine, however since upgrading to v9, the mapping setup goes missing as it's passed through the resolution context. Meaning that the resulting call of Mapper.Map<FixedBarDestinationObject>() always returns null.

I understand that the ResolutionContext may have changed slightly, but I don't understand how to resolve this issue and ensure that the mock mapping is passed through to the underlying converter.

Thank you for any help or advice.

wombat172a
  • 111
  • 6
  • You don't need to mock anything. Just use the real mapper in tests. – Lucian Bargaoanu Aug 19 '20 at 04:38
  • Okay brill, so perhaps this is where I'm getting confused. Currently the Converter class is getting it's Mapper from the ResolutionContext that is passed into it, and I'm assuming that this is still correct. So, should either 1) The test class be loading the actual Mapper (as 'IMapper'), and then new up a ResolutionContext to pass in - if so how because ResolutionContext is expecting an IRuntimeMapper? 2) I currently have a unit test class that covers the Profile used, should that be extended to cover the converter? Thanks again – wombat172a Aug 19 '20 at 07:17
  • 2 I guess :) Check the tests in the AM repo. – Lucian Bargaoanu Aug 19 '20 at 07:36
  • 1
    I am facing the same issue but I didn't get solution to your answer. I am writing unit test case for my converter so why do I pass actual mapper? – shahista inamdar Sep 15 '20 at 10:54

1 Answers1

5

Thanks to Lucian I finally got my head around this:

public class FooBarConverterTests
{
    private readonly FooBarConverter target;

    private readonly IMapper mapper;

    public FooBarConverterTests()
    {
        this.mapper = this.GetMapperConfiguration().CreateMapper();
        
        this.target = new FooBarConverter();
    }

    [Fact]
    public void FixedAmountFooModel_ConvertsTo_FixedBarDomainModel()
    {
        // Arrange
        var input = new Foo
        {
            Type = SomeType.FixedAmount
        };

        var expected = new DomainModels.FixedBarDestinationObject();

        // Act
        var actual = this.Mapper.Map<BarDestinationObjectdestination>(input);

        // Assert
        actual.ShouldSatisfyAllConditions(
            () => actual.ShouldNotBeNull(),
            () => actual.ShouldBeAssignableTo<DomainModels.FixedBarDestinationObject>());
    }
    
        private MapperConfiguration GetMapperConfiguration()
        {
            return new MapperConfiguration(opt =>
            {
                opt.AddProfile<CustomAutomapperProfile>();
                opt.ConstructServicesUsing(t =>
                {
                    if (t == typeof(FooBarConverter))
                    {
                        return this.target;
                    }

                    return null;
                });
            });
        }
}

So, I'm loading the mapper profile (which requires the converter) and call the converter through that, this ensures that the mapper profile is loaded.

As a bonus, this also means that I entirely do away with newing up the ResolutionContext and paves the way for upgrading to v10.

wombat172a
  • 111
  • 6