使用AutoMapper ProjectTo和Moq.EntityFrameworkCore进行单元测试

5

我有一些通过依赖注入接收DbContext的类需要测试。 我正在使用AutoMapper的ProjectTo,因为我的实体通常比我从我的类返回的对象(dto)大得多。 我很喜欢AutoMapper调整我的查询,使其仅选择在我的DTO中的字段。

我一直在尝试使用Moq.EntityFrameworkCore模拟我的DbContext。 它运作得相对顺利,但它确实导致了与AutoMapper ProjectTo()的问题。 最终我遇到了InvalidCastException。

显然,我不感兴趣“测试AutoMapper”或我的DbContext,我只想测试我的代码。 但是,由于投影崩溃,我无法测试我的代码。

这是一个最小化的重现,使用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>

ProjectTo 的测试与模拟无关。你需要一个真正的提供者。一个真正的内存数据库虽然快,但仍不是真正的东西。你需要更好地理解 ProjectTo 的作用以及如何进行测试。 - Lucian Bargaoanu
1
@LucianBargaoanu 嗯,问题就在这里,我对测试 ProjectTo(或任何 Automapper 函数或 EF Core)没有兴趣。但是我确实在我的代码中使用它们。在示例中,显然我没有任何逻辑,但在我的真实应用程序中有。我只想测试在 ProjectTo 之前或之后出现的自己的逻辑。由于它只会抛出异常并破坏我的测试,所以我现在无法测试自己的逻辑。此外,我确信我理解 ProjectTo 并且正在使用它来执行其预期功能(根据映射发出可查询的内容,避免选择未映射的属性)。 - Carl Quirion
为了清晰起见,我在示例中添加了一些代码注释,以概述我想要测试自己的代码,而不是 AutoMapper 的 ProjectTo。 - Carl Quirion
为什么不直接模拟“IMapper”呢? - Lucian Bargaoanu
2
简单吗?那似乎是最复杂的事情。模拟IMapper足够简单,但为ProjectTo提供功能性模拟却是另一回事。例如,任何对ToListAsync()的调用都需要实现IAsyncEnumerable的IQueryable。尝试使用MoqQueryable,但没有成功。我认为较少涉及的方式是使用InMemory数据库提供程序。虽然我不是很喜欢这样做,但它确实起作用。 - Carl Quirion
人们以前就做过这件事。我并不是说要从头开始 :) - Lucian Bargaoanu
1个回答

2
我前几天遇到了类似的问题,但最终我能够运行我的单元测试。
我使用MockQueryable.Moqhttps://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应该在集成测试中使用,而不是在单元测试中使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接