简体   繁体   中英

.Net Core Unit Test Error - The source IQueryable doesn't implement IAsyncEnumerable<…>

I have a line of code that is failing in a unit test, but is working perfectly fine in development and production.

var result = await _mapper.ProjectTo<GetApplicationsResponse.Application>(pipelineContext.Query).ToListAsync(cancellationToken);

pipelineContext.Query is type of IQueryable .

The test that I am trying to conduct is as follows

[Fact]
public async Task Handle_Success_Returns_GetApplicationsResponse()
{
    //Arrange
    var sut = CreateSut();

    _pipelineSteps
        .Setup(steps => steps.GetEnumerator())
        .Returns(() => new List<IPipelineStep<GetApplicationsContext>>
        {
            Mock.Of<IPipelineStep<GetApplicationsContext>>()
        }.GetEnumerator());

    _mapper.Setup(x => x.ConfigurationProvider)
        .Returns(
            () => new MapperConfiguration(
                cfg =>
                {
                    cfg.CreateMap<Entities.ApplicationsAggregate.Application, GetApplicationsResponse.Application>();
                    cfg.CreateMap<Entities.ApplicationsAggregate.SiteLocation, GetApplicationsResponse.SiteLocation>();
                    cfg.CreateMap<Entities.ApplicationsAggregate.SiteAddress, GetApplicationsResponse.SiteAddress>();
                }));

    //Act
    var result = await sut.Handle(new GetApplicationsRequest(), default);
    
    //Assert
    result.Should().BeOfType<GetApplicationsResponse>();
    _pipelineSteps.Verify(steps => steps.GetEnumerator(), Times.Once);
}

Limitations that I have in this is that I cannot change from _projectTo<...> because this is the new method \\ standard of working.

So, I would appreciate any help in being able to get passed this error

System.InvalidOperationException : The source IQueryable doesn't implement IAsyncEnumerable<TQ.Applications.Application.Queries.GetApplications.GetApplicationsResponse+Application>. Only sources that implement IAsyncEnumerable can be used for Entity Framework asynchronous operations.

---- Edit ---

Forgot to mention previously that the Test is using an in memory database

The problem will be that ToListAsync wants a sequence that implements IAsyncEnumerable, but ProjectTo isn't giving it one.

You are using the EntityFrameworkCore in-memory provider and I assume you are injecting it into the SUT and it being is referenced at the point of failure. This is the main issue as the in-memory provider doesn't serve up sequences that implement IAsyncEnumerable. ProjectTo ends up providing an IQueryable<T> to ToListAsync which won't work.

As to how to solve it, there are a couple of ways.

  1. The lazy/right way: use a better DbContext.

The following LINQPad example uses EntityFrameworkCore.Testing.Moq to create an injectable DbContext that produces IAsyncEnumerable sequences:

void Main()
{
    var fixture = new Fixture();

    var dataEntites = fixture.CreateMany<DataEntity>();
    var expectedResult = dataEntites.Select(x => new BusinessEntity() { id = x.Id, code = x.Code });

    var mapper = new Mapper(new MapperConfiguration(x => x.AddProfile(new MappingProfile())));
    var pipelineContext = Create.MockedDbContextFor<PipelineContext>();
    pipelineContext.Entities.AddRangeToReadOnlySource(dataEntites);

    var sut = new SUT(mapper, pipelineContext);

    var actualResult = sut.Handle().Result;

    var compareLogic = new CompareLogic();
    compareLogic.Config.IgnoreObjectTypes = true;
    compareLogic.Config.IgnoreCollectionOrder = true;
    var comparisonResult = compareLogic.Compare(expectedResult, actualResult);
    Console.WriteLine($"Are the sequences equivalent: {comparisonResult.AreEqual}");
    Console.WriteLine(expectedResult);
    Console.WriteLine(actualResult);
}

public class SUT
{
    IMapper _mapper;
    PipelineContext _pipelineContext;

    public SUT(IMapper mapper, PipelineContext pipelineContext)
    {
        _pipelineContext = pipelineContext;
        _mapper = mapper;
    }

    public async Task<List<BusinessEntity>> Handle()
    {
        return await _mapper.ProjectTo<BusinessEntity>(_pipelineContext.Entities).ToListAsync();
    }
}

public class PipelineContext : DbContext
{
    public PipelineContext(DbContextOptions<PipelineContext> options) : base(options) { }

    public virtual DbSet<DataEntity> Entities { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<DataEntity>().HasNoKey().ToView(nameof(DataEntity));
    }
}

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<DataEntity, BusinessEntity>()
        .ForMember(d => d.id, o => o.MapFrom(s => s.Id))
        .ForMember(d => d.code, o => o.MapFrom(s => s.Code))
        .ReverseMap();
    }
}

public class DataEntity
{
    public Guid Id { get; set; }

    public string Code { get; set; }
}

public class BusinessEntity
{
    public Guid id { get; set; }

    public string code { get; set; }
}

This returns:

在此处输入图片说明

Obviously I've simplified this in the absence of a minimal reproducible example but that shouldn't change the approach. I've assumed the set is read-only based on the property name (Query) and thus arranged using AddToReadOnlySource. If it's not read-only you'd use AddRange instead.

  1. Mock the mapper.

I use a real mapper most of the time based on commentary on the subject by JBogard. However as it seems you're open to mocking it you could simply mock the ProjectTo invocation to return the desired IAsyncEnumerable sequence:

void Main()
{
    var fixture = new Fixture();
    
    var dataEntites = new AsyncEnumerable<DataEntity>(fixture.CreateMany<DataEntity>());
    var expectedResult = new AsyncEnumerable<BusinessEntity>(dataEntites.Select(x => new BusinessEntity() { id = x.Id, code = x.Code }));

    var mapperMock = new Mock<IMapper>();
    mapperMock.Setup(x => x.ProjectTo<BusinessEntity>(It.IsAny<IQueryable<DataEntity>>(), It.IsAny<object>())).Returns(expectedResult);
    var mapper = mapperMock.Object;

    var sut = new SUT(mapper);

    var actualResult = sut.Handle(dataEntites).Result;

    var compareLogic = new CompareLogic();
    compareLogic.Config.IgnoreObjectTypes = true;
    compareLogic.Config.IgnoreCollectionOrder = true;
    var comparisonResult = compareLogic.Compare(expectedResult, actualResult);
    Console.WriteLine($"Are the sequences equivalent: {comparisonResult.AreEqual}");
    Console.WriteLine(expectedResult);
    Console.WriteLine(actualResult);
}

public class SUT
{
    IMapper _mapper;

    public SUT(IMapper mapper)
    {
        _mapper = mapper;
    }

    public async Task<List<BusinessEntity>> Handle(IQueryable<DataEntity> entities)
    {
        return await _mapper.ProjectTo<BusinessEntity>(entities).ToListAsync();
    }
}

public class DataEntity
{
    public Guid Id { get; set; }

    public string Code { get; set; }
}

public class BusinessEntity
{
    public Guid id { get; set; }

    public string code { get; set; }
}

And the outcome:

在此处输入图片说明

This uses the AsyncEnumerable class from EntityFrameworkCore.Testing which you could use as is or as the basis for your own implementation should you wish.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM