Модульные тесты с AutoMapper ProjectTo и Moq.EntityFrameworkCore - PullRequest
1 голос
/ 16 июня 2020

У меня есть классы, которые получают DbContexts через внедрение зависимостей, и я хотел бы протестировать их. Я использую ProjectTo AutoMapper, поскольку мои объекты часто намного больше, чем объекты (dto), которые я возвращаю из своего класса. Мне очень нравится, когда AutoMapper настраивает мой запрос так, чтобы он выбирал только поля, которые находятся в моих DTO.

Я пытался имитировать свой DbContext, используя Moq.EntityFrameworkCore. Он работает относительно хорошо, но вызывает проблемы с 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>

1 Ответ

0 голосов
/ 19 июня 2020

На днях я столкнулся с аналогичной проблемой, но, наконец, смог запустить свои модульные тесты.

Я использую 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 следует использовать в интеграционных тестах, а не в модульных тестах.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...