Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Mock an Entity Framework 6 Async Projecting Query

By leveraging the Testing with async queries section of the Testing with a Mocking Framework article on MSDN, I've been able to create many successfully passing tests.

Here's my test code, which uses NSubstitute for mocks:

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() doesn't do much, but it makes the following query:

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

This works fine until I add a projection into the query:

await DataContext.Locations
    .GroupBy(l => l.Area)
    .Select(l => new LocationsIndexVM{ Area = l.Key }) // added projection
    .ToListAsync());

which results in this exception:

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

UPDATE: I've uploaded a small, simple solution that reproduces this problem.

Can anyone provide an example of what is required to unit test a query that is both async and contains a .Select() projection?

like image 983
nikmd23 Avatar asked Mar 31 '14 19:03

nikmd23


1 Answers

So I did a bit of digging, and the issue is to do with the way the TestDbAsyncEnumerable<T> exposes the IQueryProvider. My best guess as to the reasoning is below, and the solution below that.

TestDbAsyncEnumerable<T> inherits from EnumerableQuery<T>, which in turn inherits from IQueryable<T>, and explicitly implements the Provider property of this interface:

IQueryProvider IQueryable.Provider { get ... }

Given that it's implemented explicitly, I am assuming that the LINQ internals explicitly cast a type before trying to get the Provider:

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

I don't have a source on hand (and can't be bothered looking for one), but I believe the type binding rules are different for explicit implementations; essentially, the Provider property on your TestDbAsyncEnumerable<T> is not considered to be an implementation of IQueryable<T>.Provider as an explicit one exists further up the chain, so your TestDbAsyncQueryProvider<T> is never returned.

The fix for this is to make TestDbAsyncEnumerable<T> also inherit IQueryable<T> and explicitly implement the Provider property, as below (adjusted from the MSDN article you linked):

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); }
    }
}
like image 94
jeffora Avatar answered Oct 15 '22 01:10

jeffora