.net-core - 使用 AutoMapper ProjectTo 和 Moq.EntityFrameworkCore 进行单元测试
问题描述
我有一些通过依赖注入接收 DbContexts 的类,我想对其进行测试。我正在使用 AutoMapper 的 ProjectTo,因为我的实体通常比我从班级返回的对象 (dto) 大得多。我真的很喜欢让 AutoMapper 调整我的查询,以便它只选择我的 DTO 中的字段。
我一直在尝试使用 Moq.EntityFrameworkCore 来模拟我的 DbContext。它工作得相对较好,但它确实会导致 AutoMapper ProjectTo() 出现问题。我最终得到了 InvalidCastException。
显然,我对“测试 AutoMapper”或我的 DbContext 不感兴趣,我只想测试我的代码。但是,我无法测试我的代码,因为它在 Projections 上崩溃了。
这是一个极简主义的重现,使用 AutoFixture 稍微缩短代码,我将所有内容都放入一个文件中,以便任何人都可以轻松尝试:
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>();
}
}
}
应该输出以下错误:
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
知道如何继续使用 AutoMapper 进行预测,而且还可以进行单元测试吗?
作为参考,这是我的项目文件内容:
<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>
解决方案
前几天我遇到了类似的问题,但最后我能够运行我的单元测试。
我使用MockQueryable.Moq
(https://github.com/romantitov/MockQueryable)进行模拟DBSet
,您可能应该尝试这个包,因为这里可能存在问题。
所以我的代码看起来像这样:
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
};
}
}
简化的单元测试如下所示:
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);
}
}
我同意这是一个值得商榷的问题,我们应该如何DBSet
在单元测试中模拟。我认为这InMemoryDatabase
应该用于集成测试而不是单元测试。
推荐阅读
- c++ - 用 x86/x64 msvc 的 Intrinsics 替换内联汇编 tailcall 函数结尾
- android - 运行 DJI UX SDK 4.7 的问题
- emacs - 组织模式下每周议程视图的命令
- security - 本地到本地 CouchDB 连续复制的身份验证
- javascript - 禁用 AngularJS 表单中选择标记内的选项
- html - 将数字与对话框中的值进行比较
- sql - 选择与特定记录相关但没有其他记录相关的记录
- python - 标签中的小部件放置
- c# - 如何使用单声道将 .so 文件链接到托管 DLL
- r - 除非激活另一个小部件,否则闪亮的小部件不起作用