首页 > 解决方案 > 使用 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>

标签: .net-coreautomappermoqxunit

解决方案


前几天我遇到了类似的问题,但最后我能够运行我的单元测试。

我使用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应该用于集成测试而不是单元测试。


推荐阅读