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>