Testing EF async methods with sync methods with MOQ
asynchronous, c#, entity-framework, moq, unit-testing
Solution
The `EnumerableQuery<T>` class which is produced by `.AsQueryable()` does not implement `IDbAsyncQueryProvider` but it's easy to extend `EnumerableQuery<T>` with the implementation. Create one of these instead of calling `.AsQueryable()` to wrap your collection. I have an implementation below that extends it further into a `IDbSet<T>` but you may not need to go that far.
class StubSet<T> : EnumerableQuery<T>, IDbSet<T>, IDbAsyncQueryProvider
where T : class
{
public StubSet(IEnumerable<T> collection) : base(collection)
{
Local = new ObservableCollection<T>(collection);
}
public ObservableCollection<T> Local { get; private set; }
public T Find(params object[] keyValues)
{
throw new NotImplementedException();
}
public T Add(T entity)
{
Local.Add(entity);
return entity;
}
public T Remove(T entity)
{
Local.Remove(entity);
return entity;
}
public T Attach(T entity)
{
return Add(entity);
}
public T Create()
{
throw new NotImplementedException();
}
public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T
{
throw new NotImplementedException();
}
public void DeleteObject(T entity)
{
throw new NotImplementedException();
}
public void Detach(T entity)
{
throw new NotImplementedException();
}
async Task<object> IDbAsyncQueryProvider.ExecuteAsync(Expression expression, CancellationToken cancellationToken)
{
return ((IQueryProvider)this).Execute(expression);
}
async Task<TResult> IDbAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return ((IQueryProvider)this).Execute<TResult>(expression);
}
}
Problem
I have this method: ``` public async Task DeleteUserAsync(Guid userId) { using (var context = this.contextFactory.Create()) { var user = await context.Users.FirstOrDefaultAsync(x => x.Id.Equals(userId)); if (user == null) { throw new Exception("User doesn't exist"); } context.Users.Remove(user); await context.SaveChangesAsync(); } } ``` I want to test it out. So I create the test: ``` [TestMethod] public async Task DeleteUsersSuccessfulCallTest() { // Arrange var id = Guid.NewGuid(); var user = new User() { Id = id }; var context = new Mock<IDashboardContext>(); var usersDbSet = DbSetQueryMocking.GenericSetupAsyncQueryableMockInterfaceSet(new List<User> { user }.AsQueryable()); context.Setup(x => x.Users).Returns(usersDbSet.Object); context.Setup(x => x.Users.Remove(user)).Returns(user).Verifiable(); context.Setup(x => x.SaveChangesAsync()).ReturnsAsync(1).Verifiable(); this.contextFactory.Setup(x => x.Create()).Returns(context.Object); // Act await this.userService.DeleteUserAsync(id); context.VerifyAll(); } } ``` I have got this method to create me a mock set: ``` public static Mock<DbSet<T>> GenericSetupAsyncQueryableMockSet<T>(IQueryable<T> data) where T : class { var mockSet = new Mock<DbSet<T>>(); mockSet.As<IDbAsyncEnumerable<T>>().Setup(m => m.GetAsyncEnumerator()).Returns(new TestDbAsyncEnumerator<T>(data.GetEnumerator())); mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestDbAsyncQueryProvider<T>(data.Provider)); mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); return mockSet; } ``` However, because my `DeleteUserAsync` contains async extension methods and standard sync methods I get this error message: System.InvalidOperationException: The provider for the source IQueryable doesn't implement IDbAsyncQueryProvider. Only providers that implement IDbAsyncQueryProvider can be used for Entity Framework asynchronous operations. For more details see http://go.microsoft.com/fwlink/?LinkId=287068. Obviously if I just set up the `DbSet<T>` with `Queryable` mocked out then it will throw the same exception. FYI: the offending line is: ``` context.Setup(x => x.Users.Remove(user)).Returns(user).Verifiable(); ``` With this line: errors Without it: a successful test. How do I fix this?