如何模拟一个 Entity Framework 6 异步投影查询

8

通过利用MSDN上《使用模拟框架进行测试》文章中异步查询测试部分的链接,我成功创建了许多测试用例。

这是我的测试代码,使用NSubstitute进行模拟:

var dummyQueryable = locations.AsQueryable();

var mock = Substitute.For<DbSet<Location>, IDbAsyncEnumerable<Location>, IQueryable<Location>>();
((IDbAsyncEnumerable<Location>)mock).GetAsyncEnumerator().Returns(new TestDbAsyncEnumerator<Location>(dummyQueryable.GetEnumerator()));
((IQueryable<Location>)mock).Provider.Returns(new TestDbAsyncQueryProvider<Location>(dummyQueryable.Provider));
((IQueryable<Location>)mock).Expression.Returns(dummyQueryable.Expression);
((IQueryable<Location>)mock).ElementType.Returns(dummyQueryable.ElementType);
((IQueryable<Location>)mock).GetEnumerator().Returns(dummyQueryable.GetEnumerator());
sut.DataContext.Locations = mock;

var result = await sut.Index();

result.Should().BeView();

sut.Index() 并没有做太多事情,但它会执行以下查询:

await DataContext.Locations
    .GroupBy(l => l.Area)
    .ToListAsync());

这个功能在查询中添加投影之前运行良好:
await DataContext.Locations
    .GroupBy(l => l.Area)
    .Select(l => new LocationsIndexVM{ Area = l.Key }) // added projection
    .ToListAsync());

这会导致以下异常:
System.InvalidOperationException
The source IQueryable doesn't implement IDbAsyncEnumerable<LocationsIndexVM>. Only sources that implement IDbAsyncEnumerable can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068.
   at System.Data.Entity.QueryableExtensions.AsDbAsyncEnumerable(IQueryable`1 source)
   at System.Data.Entity.QueryableExtensions.ToListAsync(IQueryable`1 source)
   at Example.Web.Controllers.HomeController.<Index>d__0.MoveNext() in HomeController.cs: line 25
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Example.Test.Web.Controllers.HomeControllerShould.<TempTest>d__4.MoveNext() in HomeControllerShould.cs: line 71
更新: 我已经上传了一个小而简单的解决方案,可重现此问题。
有人能提供一个示例来对既是async又包含.Select()投影的查询进行单元测试吗?
1个回答

16

我进行了一些调查,问题与 TestDbAsyncEnumerable<T> 暴露 IQueryProvider 的方式有关。以下是我对其原因的最佳猜测,以及解决方案。

TestDbAsyncEnumerable<T> 继承自 EnumerableQuery<T>,后者又继承自 IQueryable<T>,并显式实现此接口的 Provider 属性:

IQueryProvider IQueryable.Provider { get ... }

鉴于它是明确实现的,我假设LINQ内部在尝试获取Provider之前会明确转换类型:

((IQueryable<T>)source).Provider.CreateQuery(...);

我手头没有资料(也不想去找),但我认为显式实现的类型绑定规则是不同的;基本上,你的TestDbAsyncEnumerable<T>上的Provider属性不被认为是IQueryable<T>.Provider的一种实现,因为更高层次上存在显式的实现,所以你的TestDbAsyncQueryProvider<T>永远不会被返回。

解决方法是使TestDbAsyncEnumerable<T>也继承自IQueryable<T>并显式实现Provider属性,如下所示(从你链接的MSDN文章中调整):

public class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, IDbAsyncEnumerable<T>, IQueryable<T>
{
    public TestDbAsyncEnumerable(IEnumerable<T> enumerable) : base(enumerable)
    { }

    public TestDbAsyncEnumerable(Expression expression) : base(expression)
    { }

    public IDbAsyncEnumerator<T> GetAsyncEnumerator()
    {
        return new TestDbAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IDbAsyncEnumerator IDbAsyncEnumerable.GetAsyncEnumerator()
    {
        return GetAsyncEnumerator();
    }

    IQueryProvider IQueryable.Provider
    {
        get { return new TestDbAsyncQueryProvider<T>(this); }
    }
}

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