21

I'm using XUNIT to test in a dot net core application.

I need to test a service that is internally making an async query on a DbSet in my datacontext.

I've seen here that mocking that DbSet asynchronously is possible.

The problem I'm having is that the IDbAsyncQueryProvider does not seem to be available in EntityframeworkCore, which I'm using.

Am I incorrect here? Has anyone else got this working?

(Been a long day, hopefully I'm just missing something simple)

EDIT

After asking on GitHub, I got point to this class: https://github.com/aspnet/EntityFramework/blob/dev/src/Microsoft.EntityFrameworkCore/Query/Internal/IAsyncQueryProvider.cs

This is what I've gotten to so far in trying to implement this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Query.Internal;

namespace EFCoreTestQueryProvider
{
    internal class TestAsyncQueryProvider<TEntity>: IAsyncQueryProvider
    {
        private readonly IQueryProvider _inner;

        internal TestAsyncQueryProvider(IQueryProvider inner)
        {
            _inner = inner;
        }

        IQueryable CreateQuery(Expression expression)
        {
            return new TestDbAsyncEnumerable<TEntity>(expression);
        }

        IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
             return new TestDbAsyncEnumerable<TElement>(expression);
        }

        object Execute(Expression expression)
        {
            return _inner.Execute(expression);
        }

        TResult Execute<TResult>(Expression expression)
        {
            return _inner.Execute<TResult>(expression);
        }

        IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
        {
            return Task.FromResult(Execute<TResult>(expression)).ToAsyncEnumerable();
        }

        Task<TResult> IAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
        {
            return Task.FromResult(Execute<TResult>(expression));
        }
    }

    internal class TestDbAsyncEnumerable<T> : EnumerableQuery<T>, System.Collections.Generic.IAsyncEnumerable<T>, IQueryable<T>
    {
        public TestDbAsyncEnumerable(IEnumerable<T> enumerable)
            : base(enumerable)
        { }

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

        public IAsyncEnumerator<T> GetAsyncEnumerator()
        {
            return new TestDbAsyncEnumerable<T>(this.AsEnumerable()).ToAsyncEnumerable();
        }

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

        IAsyncEnumerator<T> IAsyncEnumerable<T>.GetEnumerator()
        {
            throw new NotImplementedException();
        }

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

I've now tried to implement this and have run into some more issues, specifically around these two methods:

public IAsyncEnumerator<T> GetAsyncEnumerator()
{
    return new TestDbAsyncEnumerable<T>(this.AsEnumerable()).ToAsyncEnumerable();
}

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

I'm hoping that somebody could point me in the right direction as to what I'm doing wrong.

Maximilian Riegler
  • 22,720
  • 4
  • 62
  • 71
Chris
  • 7,996
  • 11
  • 66
  • 98

3 Answers3

27

I finally got this to work. They slightly changed the interfaces in EntityFrameworkCore from IDbAsyncEnumerable to IAsyncEnumerable so the following code worked for me:

public class AsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
    public AsyncEnumerable(Expression expression)
        : base(expression) { }

    public IAsyncEnumerator<T> GetEnumerator() =>
        new AsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}

public class AsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> enumerator;

    public AsyncEnumerator(IEnumerator<T> enumerator) =>
        this.enumerator = enumerator ?? throw new ArgumentNullException();

    public T Current => enumerator.Current;

    public void Dispose() { }

    public Task<bool> MoveNext(CancellationToken cancellationToken) =>
        Task.FromResult(enumerator.MoveNext());
}

[Fact]
public async Task TestEFCore()
{
    var data =
        new List<Entity>()
        {
            new Entity(),
            new Entity(),
            new Entity()
        }.AsQueryable();

    var mockDbSet = new Mock<DbSet<Entity>>();

    mockDbSet.As<IAsyncEnumerable<Entity>>()
        .Setup(d => d.GetEnumerator())
        .Returns(new AsyncEnumerator<Entity>(data.GetEnumerator()));

    mockDbSet.As<IQueryable<Entity>>().Setup(m => m.Provider).Returns(data.Provider);
    mockDbSet.As<IQueryable<Entity>>().Setup(m => m.Expression).Returns(data.Expression);
    mockDbSet.As<IQueryable<Entity>>().Setup(m => m.ElementType).Returns(data.ElementType);
    mockDbSet.As<IQueryable<Entity>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

    var mockCtx = new Mock<SomeDbContext>();
    mockCtx.SetupGet(c => c.Entities).Returns(mockDbSet.Object);

    var entities = await mockCtx.Object.Entities.ToListAsync();

    Assert.NotNull(entities);
    Assert.Equal(3, entities.Count());
}

You might be able to clean up those test implementations of the AsyncEnumerable and AsyncEnumerator even more. I didn't try, I just got it to work.

Remember your DbSet on your DbContext needs to be marked as virtual or else you will need to implement some interface wrapper over the DbContext to make this work properly.

Carson
  • 1,169
  • 13
  • 36
2

I got help from Carsons answer, but I had to alter his code a bit to make it work with EntityFramework Core 6.4.4 and Moq.

Here is the altered code:

internal class AsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
    public AsyncEnumerable(Expression expression)
        : base(expression) { }

    public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) =>
        new AsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}

internal class AsyncEnumerator<T> : IAsyncEnumerator<T>, IAsyncDisposable, IDisposable
{
    private readonly IEnumerator<T> enumerator;

    private Utf8JsonWriter? _jsonWriter = new(new MemoryStream());

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        Dispose(disposing: false);
        #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize

        GC.SuppressFinalize(this);
        #pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _jsonWriter?.Dispose();
            _jsonWriter = null;
        }
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_jsonWriter is not null)
        {
            await _jsonWriter.DisposeAsync().ConfigureAwait(false);
        }

        _jsonWriter = null;
    }

    public AsyncEnumerator(IEnumerator<T> enumerator) =>
        this.enumerator = enumerator ?? throw new ArgumentNullException();

    public T Current => enumerator.Current;

    public ValueTask<bool> MoveNextAsync() =>
        new ValueTask<bool>(enumerator.MoveNext());
}

internal class TestAsyncQueryProvider<TEntity> : IDbAsyncQueryProvider
{
    private readonly IQueryProvider _inner;

    internal TestAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }

    public IQueryable CreateQuery(Expression expression)
    {
        return new AsyncEnumerable<TEntity>(expression);
    }

    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new AsyncEnumerable<TElement>(expression);
    }

    public object Execute(Expression expression)
    {
        return _inner.Execute(expression);
    }

    public TResult Execute<TResult>(Expression expression)
    {
        return _inner.Execute<TResult>(expression);
    }

    public Task<object> ExecuteAsync(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute(expression));
    }

    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}

A helper class:

public static class MockDbSet
{       
    public static Mock<DbSet<TEntity>> BuildAsync<TEntity>(List<TEntity> data) where TEntity : class
    {
        var queryable = data.AsQueryable();

        var mockSet = new Mock<DbSet<TEntity>>();
        mockSet.As<IAsyncEnumerable<TEntity>>()
            .Setup(d => d.GetAsyncEnumerator(It.IsAny<CancellationToken>()))
            .Returns(new AsyncEnumerator<TEntity>(queryable.GetEnumerator()));

        mockSet.As<IQueryable<TEntity>>()
            .Setup(m => m.Provider)
            .Returns(new TestAsyncQueryProvider<TEntity>(queryable.Provider));

        mockSet.As<IQueryable<TEntity>>().Setup(m => m.Expression).Returns(queryable.Expression);
        mockSet.As<IQueryable<TEntity>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        mockSet.As<IQueryable<TEntity>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());

        mockSet.Setup(m => m.Add(It.IsAny<TEntity>())).Callback<TEntity>(data.Add);
        return mockSet;
    }
}

Mocking async unit test (MSTestV2):

    [TestMethod]
    public async Task GetData_Should_Not_Return_Null()
    {
        // Arrange
        var data = new List<Entity>()
        {
            new Entity()
        };

        _mockContext.Setup(m => m.Entitys).Returns(MockDbSet.BuildAsync(data).Object);

        // Act 
        var actual = await _repository.GetDataAsync();

        // Assert
        Assert.IsNotNull(actual);
    }
Tegge
  • 329
  • 3
  • 7
0

See here to mock DbContext with async queryable support: https://stackoverflow.com/a/71076807/4905704

Like this:

    [Fact]
    public void Test()
    {
        var testData = new List<MyEntity>
        {
            new MyEntity() { Id = Guid.NewGuid() },
            new MyEntity() { Id = Guid.NewGuid() },
            new MyEntity() { Id = Guid.NewGuid() },
        };

        var mockDbContext = new MockDbContextAsynced<MyDbContext>();
        mockDbContext.AddDbSetData<MyEntity>(testData.AsQueryable());

        mockDbContext.MyEntities.ToArrayAsync();
        // or
        mockDbContext.MyEntities.SingleAsync();
        // or etc.
        
        // To inject MyDbContext as type parameter with mocked data
        var mockService = new SomeService(mockDbContext.Object);
    }
Leonid Pavlov
  • 671
  • 8
  • 13