AutoMapper / AutoMapper.Extensions.Microsoft.DependencyInjection

MIT License
258 stars 79 forks source link

Unit testing controllers that use resolvers with a dependency #101

Closed nikoszs closed 5 years ago

nikoszs commented 5 years ago

Hi, I'm busy setting up unit tests for my controllers. I'm mocking all services and everything works fine when automapper profiles don't use resolvers.

[Fact]
public async Task List_ReturnsNotes() 
{
    // arrange
    var mockLoggingService = new Mock<ILoggingService>();

    var config = new MapperConfiguration(cfg => {
        cfg.AddProfile(new MainMappingProfile());
        cfg.AddProfile(new UserMappingProfile());
    });
    var mapper = config.CreateMapper();
    var controller = new NotesController(mockloggingService.Object, mapper);

    // act
    var result = await controller.List(testEventIdInput); //<-- in List I call mapper.Map<>()

    // ...left out rest for brevity
}

So inside UserMappingProfile I have CreateMap<NoteDto, NoteViewModel>(). No issues and test run fine.

As soon as I start using a resolver that uses a service, my tests starts bombing.

public class CryptoEncodeResolver : IMemberValueResolver<object, object, long?, string>
{
    private readonly ICryptographyService _cryptographyService; //<-----

    public CryptoEncodeResolver(ICryptographyService cryptographyService)
    {
        _cryptographyService = cryptographyService;
    }

    public string Resolve(object source, object destination, long? sourceMember, 
        string destMember, ResolutionContext context)
    {
        return _cryptographyService.Encode(sourceMember);
    }
}

Then update my UserMappingProfile to actually use the resolver now.

CreateMap<NoteDto, NoteViewModel>()
    .ForMember(dest => dest.NoteId, opts => opts.MapFrom<CryptoEncodeResolver, long?>(src => src.NoteId));

The resolver works 💯 when when the project runs normally from Startup.cs using .net core IoC container, but in my unit testing project it fails when the resolver is used.

So I dont have a DI container that adds services.AddScoped<ICryptographyService>() in my unit testing project, which is why it works in the main project and not my unit testing project. It makes sense why its not working, but how do I make it inject that service into the resolver?

I've tried using ConstructServicesUsing with mock IServiceProvider, but it keeps telling me there's a mapping error when it hits the resolver.

My question

How can I instantiate a resolver with a mock service (ICryptoService) when setting up automapper in a unit testing project. I don't really want to mock my Startup and ConfigureServices, because then I'll have do a lot more work to mock other things. Am I missing something here?

Version

Automapper V8.1 I've also installed Automapper.DependencyInjection package.

Thanks for your time.

jbogard commented 5 years ago

One way to do this is to still call Startup in your unit test to have the container configured "correctly", then explicitly register the mock object around here: https://github.com/jbogard/ContosoUniversityDotNetCore-Pages/blob/master/ContosoUniversity.IntegrationTests/SliceFixture.cs#L33

Unfortunately, the built-in container does not allow replacing instances after the service provider is built, which is what we do with other containers - create a scope, but replace the "default" instance inside the scope with the fake. This saves us from having to configure the container every single time.

nikoszs commented 5 years ago

Thanks so much! This helped me to get to the solution.

For anyone struggling, I just explicitly instantiated/mocked the Resolver object and added it to the services before the service provider is built (like jbogard mentioned above).

var services = new ServiceCollection();

// create mock service
var mockSomeService = new Mock<ISomeService>();

// setup mock object here 
mockSomeService.Setup(x => x.DoSomething()).Returns("Something");

// create resolver with mock service
var someResolver = new SomeResolver(mockSomeService.Object);

// add resolver to service collection
services.AddSingleton<SomeResolver>(someResolver);

// add your mock service to service collection
services.AddSingleton<ISomeService>(mockSomeService);

// call your mock startup configure
// I had to create a TestingStartup class because of other dependencies,
// so this startup is basically just a lean version of the main one.
// var startup = new TestingStartup(configuration);
startup.ConfigureServices(services);

// build it
var provider = services.BuildServiceProvider();

// get your service from service provider (or just use the one already created above)
var someServiceInstance = provider.GetService<ISomeService>();
var value = someServiceInstance.DoSomething(); //gets back "Something"

// config your mapper
var config = new MapperConfiguration(cfg => {
    cfg.ConstructServicesUsing(provider.GetService);
    cfg.AddProfile(new MappingProfile());
    cfg.AddProfile(new SomeOtherMappingProfile());
});
var mapper = config.CreateMapper();

// inject it into your controller
var controller = new SomeController(mapper, someServiceInstance);

// Act
var result = await controller.List(1);

// Assert...

For simplicity sake, I placed it all together, but definitely follow what jbogard is saying, use fixtures (and mock the service there) so you dont have to configure everything every time.

I hope this helps some one.

lock[bot] commented 5 years ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.