Тестирование EF-асинхронных методов с помощью методов синхронизации с MOQ

У меня есть этот метод:

    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();
        }
    }

Я хочу проверить это. Поэтому я создаю тест:

    [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();
    }
}

У меня есть этот метод, чтобы создать мне макет:

    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;
    }

Однако, поскольку my DeleteUserAsync содержит методы расширения async и стандартные методы синхронизации, я получаю это сообщение об ошибке:

System.InvalidOperationException: поставщик для источника IQueryable не реализует IDbAsyncQueryProvider. Только асинхронные операции Entity Framework могут использоваться только провайдерами, которые реализуют IDbAsyncQueryProvider. Подробнее см. http://go.microsoft.com/fwlink/?LinkId=287068.

Очевидно, что если я просто настрою DbSet<T> с Queryable, из него выйдет из строя, то он будет генерировать одно и то же исключение.

FYI: строка нарушения:

context.Setup(x => x.Users.Remove(user)).Returns(user).Verifiable();

С помощью этой строки: ошибки

Без этого: успешный тест.

Как это исправить?

Ответ 1

Класс EnumerableQuery<T>, созданный .AsQueryable(), не реализует IDbAsyncQueryProvider, но его легко расширить EnumerableQuery<T> с помощью реализации. Создайте один из них вместо вызова .AsQueryable(), чтобы обернуть вашу коллекцию. У меня есть реализация ниже, которая расширяет ее до IDbSet<T>, но вам, возможно, не нужно заходить так далеко.

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);
    }
}