Unit Tests with AutoMapper ProjectTo and Moq.EntityFrameworkCore

Viewed 1773

I have some classes that receive DbContexts thru dependency injection, that i would like to test. I am using AutoMapper's ProjectTo, as my entities are often much bigger than the objects (dto) I am returning from my class. I really like having AutoMapper adjust my query so that it only selects fields which are in my DTOs.

I've been trying to mock my DbContext, using Moq.EntityFrameworkCore. It works relatively well but it does cause issues with AutoMapper ProjectTo(). I end up getting InvalidCastException.

Obviously, I am not interested in "testing AutoMapper" or my DbContext, I just want to test my code which is around. However, I can't test my code since it crashes on the Projections.

Here's a minimalist repro, using AutoFixture to shorten the code a bit, I've thrown everything into a single file so that it's easy for anyone to try out for themselves:

using AutoFixture;
using AutoFixture.AutoMoq;
using AutoMapper;
using Microsoft.EntityFrameworkCore;
using Moq;
using Moq.EntityFrameworkCore;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

namespace UnitTestEFMoqProjectTo
{
    public class MyBusinessFixture
    {
        private IFixture _fixture;
        public MyBusinessFixture()
        {
            _fixture = new Fixture()
                .Customize(new AutoMoqCustomization());

            var mockMapper = new MapperConfiguration(cfg =>
            {
                cfg.AddProfile(new MappingProfile());
            });
            var mapper = mockMapper.CreateMapper();
            _fixture.Register(() => mapper);
        }

        [Fact]
        public async Task DoSomething_WithMocksAndProjectTo_ValidatesMyLogic()
        {
            // Arrange 
            var mockContext = new Mock<MyDbContext>();
            mockContext.Setup(x => x.MyEntities).ReturnsDbSet(new List<MyEntity>(_fixture.CreateMany<MyEntity>(10)));
            _fixture.Register(() => mockContext.Object);
            var business = _fixture.Create<MyBusiness>();

            // Act
            await business.DoSomething();

            // Assert
            Assert.True(true);
        }
    }

    public class MyDbContext : DbContext
    {
        public virtual DbSet<MyEntity> MyEntities { get; set; }
    }
    public class MyEntity
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string SomeProperty { get; set; }
        public string SomeOtherProperty { get; set; }
    }

    public class MyDto
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    public interface IMyBusiness
    {
        Task DoSomething();
    }
    public class MyBusiness : IMyBusiness
    {
        private readonly MyDbContext _myDbContext;
        private readonly IMapper _mapper;
        public MyBusiness(MyDbContext myDbContext, IMapper mapper)
        {
            _myDbContext = myDbContext;
            _mapper = mapper;
        }
        public async Task DoSomething()
        {
            // My program's logic here, that I want to test.

            // Query projections and enumeration
            var projectedEntities = await _mapper.ProjectTo<MyDto>(_myDbContext.MyEntities).ToListAsync();

            // Some more of my program's logic here, that I want to test.
        }
    }
    public class MappingProfile : Profile
    {
        public MappingProfile()
        {
            CreateMap<MyEntity, MyDto>();
        }
    }
}

Should output the following error :

Message: 
    System.InvalidCastException : Unable to cast object of type 'Moq.EntityFrameworkCore.DbAsyncQueryProvider.InMemoryAsyncEnumerable`1[UnitTestEFMoqProjectTo.MyEntity]' to type 'System.Linq.IQueryable`1[UnitTestEFMoqProjectTo.MyDto]'.
  Stack Trace: 
    ProjectionExpression.ToCore[TResult](Object parameters, IEnumerable`1 memberPathsToExpand)
    ProjectionExpression.To[TResult](Object parameters, Expression`1[] membersToExpand)
    Extensions.ProjectTo[TDestination](IQueryable source, IConfigurationProvider configuration, Object parameters, Expression`1[] membersToExpand)
    Mapper.ProjectTo[TDestination](IQueryable source, Object parameters, Expression`1[] membersToExpand)
    MyBusiness.DoSomething() line 79
    MyBusinessFixture.DoSomething_WithMocksAndProjectTo_ShouldMap() line 39

Any idea of how I could keep doing projections with AutoMapper but also have unit tests working ?

For reference, here is my project file content :

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="AutoFixture.AutoMoq" Version="4.11.0" />
    <PackageReference Include="AutoMapper" Version="9.0.0" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="3.1.5" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.1" />
    <PackageReference Include="Moq" Version="4.14.1" />
    <PackageReference Include="Moq.EntityFrameworkCore" Version="3.1.2.1" />
    <PackageReference Include="xunit" Version="2.4.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

</Project>
1 Answers

I bumped into the similar issue the other day but finally I was able to run my unit tests.

I use MockQueryable.Moq (https://github.com/romantitov/MockQueryable) for mocking DBSet, probably, you should try this package because an issue might be here.

So my code looks like it:

public class Project
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class ProjectDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class GetProjectsHandler : IRequestHandler<GetProjectsRequest, GetProjectsResponse>
{
    private readonly IMyDbContext context;
    private readonly IMapper mapper;

    public GetProjectsHandler(IMyDbContext context,IMapper mapper)
    {
        this.context = context;
        this.mapper = mapper;
    }

    public async Task<GetProjectsResponse> Handle(GetProjectsRequest request, CancellationToken cancellationToken)
    {
        IReadOnlyList<ProjectDto> projects = await context
                .Projects
                .ProjectTo<ProjectDto>(mapper.ConfigurationProvider)
                .ToListAsync(cancellationToken);

        return new GetProjectsResponse
        {
            Projects = projects
        };
    }
}

Simplified unit test looks like the following:

    public class GetProjectsTests
    {
        [Fact]
        public async Task GetProjectsTest()
        {
            var projects = new List<Project>
            {
                new Project
                {
                    Id = 1,
                    Name = "Test"
                }
            };
            var context = new Mock<IMyDbContext>();
            context.Setup(c => c.Projects).Returns(projects.AsQueryable().BuildMockDbSet().Object);

            var mapper = new Mock<IMapper>();
            mapper.Setup(x => x.ConfigurationProvider)
                .Returns(
                    () => new MapperConfiguration(
                        cfg => { cfg.CreateMap<Project, ProjectDto>(); }));


            var getProjectsRequest = new GetProjectsRequest();
            var handler = new GetProjectsHandler(context.Object, mapper.Object);
            GetProjectsResponse response = await handler.Handle(getProjectsRequest, CancellationToken.None);


            Assert.True(response.Projects.Count == 1);
        }
    }

I agree that it's a debatable question how should we mock DBSet in unit tests. I think that InMemoryDatabase should be used in integration tests rather than unit tests.

Related