Hoe een asynchrone repository te bespotten met Entity Framework Core

Ik probeer een unit-test te maken voor een klasse die een asynchrone repository aanroept. Ik gebruik ASP.NET Core en Entity Framework Core. Mijn generieke repository ziet er zo uit.

public class EntityRepository<TEntity> : IEntityRepository<TEntity> where TEntity : class
{
    private readonly SaasDispatcherDbContext _dbContext;
    private readonly DbSet<TEntity> _dbSet;
    public EntityRepository(SaasDispatcherDbContext dbContext)
    {
        _dbContext = dbContext;
        _dbSet = dbContext.Set<TEntity>();
    }
    public virtual IQueryable<TEntity> GetAll()
    {
        return _dbSet;
    }
    public virtual async Task<TEntity> FindByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }
    public virtual IQueryable<TEntity> FindBy(Expression<Func<TEntity, bool>> predicate)
    {
        return _dbSet.Where(predicate);
    }
    public virtual void Add(TEntity entity)
    {
        _dbSet.Add(entity);
    }
    public virtual void Delete(TEntity entity)
    {
        _dbSet.Remove(entity);
    }
    public virtual void Update(TEntity entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
    }
    public virtual async Task SaveChangesAsync()
    {
        await _dbContext.SaveChangesAsync();
    }
}

Dan heb ik een serviceklasse die FindBy en FirstOrDefaultAsync aanroept op een instantie van de repository:

   public async Task<Uri> GetCompanyProductURLAsync(Guid externalCompanyID, string productCode, Guid loginToken)
    {            
        CompanyProductUrl companyProductUrl = await _Repository.FindBy(u => u.Company.ExternalCompanyID == externalCompanyID && u.Product.Code == productCode.Trim()).FirstOrDefaultAsync();
        if (companyProductUrl == null)
        {
            return null;
        }
        var builder = new UriBuilder(companyProductUrl.Url);
        builder.Query = $"-s{loginToken.ToString()}";
        return builder.Uri;
    }

Ik probeer de repository-aanroep te spotten in mijn test hieronder:

   [Fact]
    public async Task GetCompanyProductURLAsync_ReturnsNullForInvalidCompanyProduct()
    {
        var companyProducts = Enumerable.Empty<CompanyProductUrl>().AsQueryable();
        var mockRepository = new Mock<IEntityRepository<CompanyProductUrl>>();
        mockRepository.Setup(r => r.FindBy(It.IsAny<Expression<Func<CompanyProductUrl, bool>>>())).Returns(companyProducts);
        var service = new CompanyProductService(mockRepository.Object);
        var result = await service.GetCompanyProductURLAsync(Guid.NewGuid(), "wot", Guid.NewGuid());
        Assert.Null(result);
    }

Als de test de aanroep naar de repository uitvoert, krijg ik echter de volgende foutmelding:

The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IEntityQueryProvider can be used for Entity Framework asynchronous operations.

Hoe kan ik de repository op de juiste manier spotten om dit te laten werken?


Antwoord 1, autoriteit 100%

Met dank aan @Nkosi voor het wijzen op een link met een voorbeeld van hetzelfde doen in EF 6: https://msdn.microsoft.com/en-us/library/dn314429.aspx. Dit werkte niet precies zoals het is met EF Core, maar ik kon ermee beginnen en wijzigingen aanbrengen om het werkend te krijgen. Hieronder staan ​​de testklassen die ik heb gemaakt om IAsyncQueryProvider te “spotten”:

internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
{
    private readonly IQueryProvider _inner;
    internal TestAsyncQueryProvider(IQueryProvider inner)
    {
        _inner = inner;
    }
    public IQueryable CreateQuery(Expression expression)
    {
        return new TestAsyncEnumerable<TEntity>(expression);
    }
    public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
    {
        return new TestAsyncEnumerable<TElement>(expression);
    }
    public object Execute(Expression expression)
    {
        return _inner.Execute(expression);
    }
    public TResult Execute<TResult>(Expression expression)
    {
        return _inner.Execute<TResult>(expression);
    }
    public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression)
    {
        return new TestAsyncEnumerable<TResult>(expression);
    }
    public Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}
internal class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
{
    public TestAsyncEnumerable(IEnumerable<T> enumerable)
        : base(enumerable)
    { }
    public TestAsyncEnumerable(Expression expression)
        : base(expression)
    { }
    public IAsyncEnumerator<T> GetEnumerator()
    {
        return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }
    IQueryProvider IQueryable.Provider
    {
        get { return new TestAsyncQueryProvider<T>(this); }
    }
}
internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
{
    private readonly IEnumerator<T> _inner;
    public TestAsyncEnumerator(IEnumerator<T> inner)
    {
        _inner = inner;
    }
    public void Dispose()
    {
        _inner.Dispose();
    }
    public T Current
    {
        get
        {
            return _inner.Current;
        }
    }
    public Task<bool> MoveNext(CancellationToken cancellationToken)
    {
        return Task.FromResult(_inner.MoveNext());
    }
}

En hier is mijn bijgewerkte testcase die deze klassen gebruikt:

[Fact]
public async Task GetCompanyProductURLAsync_ReturnsNullForInvalidCompanyProduct()
{
    var companyProducts = Enumerable.Empty<CompanyProductUrl>().AsQueryable();
    var mockSet = new Mock<DbSet<CompanyProductUrl>>();
    mockSet.As<IAsyncEnumerable<CompanyProductUrl>>()
        .Setup(m => m.GetEnumerator())
        .Returns(new TestAsyncEnumerator<CompanyProductUrl>(companyProducts.GetEnumerator()));
    mockSet.As<IQueryable<CompanyProductUrl>>()
        .Setup(m => m.Provider)
        .Returns(new TestAsyncQueryProvider<CompanyProductUrl>(companyProducts.Provider));
    mockSet.As<IQueryable<CompanyProductUrl>>().Setup(m => m.Expression).Returns(companyProducts.Expression);
    mockSet.As<IQueryable<CompanyProductUrl>>().Setup(m => m.ElementType).Returns(companyProducts.ElementType);
    mockSet.As<IQueryable<CompanyProductUrl>>().Setup(m => m.GetEnumerator()).Returns(() => companyProducts.GetEnumerator());
    var contextOptions = new DbContextOptions<SaasDispatcherDbContext>();
    var mockContext = new Mock<SaasDispatcherDbContext>(contextOptions);
    mockContext.Setup(c => c.Set<CompanyProductUrl>()).Returns(mockSet.Object);
    var entityRepository = new EntityRepository<CompanyProductUrl>(mockContext.Object);
    var service = new CompanyProductService(entityRepository);
    var result = await service.GetCompanyProductURLAsync(Guid.NewGuid(), "wot", Guid.NewGuid());
    Assert.Null(result);
}

Antwoord 2, autoriteit 58%

Probeer mijn Moq/NSubstitute/FakeItEasy-extensie MockQueryablete gebruiken:
ondersteunde alle Sync/Async-bewerkingen (bekijk hier)

//1 - create a List<T> with test items
var users = new List<UserEntity>()
{
 new UserEntity,
 ...
};
//2 - build mock by extension
var mock = users.AsQueryable().BuildMock();
//3 - setup the mock as Queryable for Moq
_userRepository.Setup(x => x.GetQueryable()).Returns(mock.Object);
//3 - setup the mock as Queryable for NSubstitute
_userRepository.GetQueryable().Returns(mock);

DbSet ook ondersteund

//2 - build mock by extension
var mock = users.AsQueryable().BuildMockDbSet();
//3 - setup DbSet for Moq
var userRepository = new TestDbSetRepository(mock.Object);
//3 - setup DbSet for NSubstitute
var userRepository = new TestDbSetRepository(mock);

Opmerkingen:

  • AutoMapper wordt ook ondersteund vanaf 1.0.4 ver
  • DbQuery ondersteund vanaf 1.1.0 ver
  • EF Core 3.0 ondersteund vanaf 3.0.0 ver
  • .Net 5 ondersteundvanaf 5.0.0 ver

Antwoord 3, autoriteit 8%

Veel minder code-oplossing. Gebruik de in-memory db-context die ervoor moet zorgen dat alle sets voor u worden opgestart. U hoeft de DbSet niet langer op uw context te spotten, maar als u bijvoorbeeld gegevens van een service wilt retourneren, kunt u eenvoudig de daadwerkelijk ingestelde gegevens van de in-memory context retourneren.

DbContextOptions< SaasDispatcherDbContext > options = new DbContextOptionsBuilder< SaasDispatcherDbContext >()
  .UseInMemoryDatabase(Guid.NewGuid().ToString())
  .Options;
  _db = new SaasDispatcherDbContext(optionsBuilder: options);

Antwoord 4, autoriteit 2%

Ik onderhoud twee open-sourceprojecten die het zware werk doen van het opzetten van de mocks en die feitelijk SaveChanges(Async)emuleren.

Voor EF Core: https://github.com/huysentruitw/entity-framework -core-mock

Voor EF6: https://github.com/huysentruitw/entity-framework-mock

Beide projecten hebben Nuget-pakketten met integratie voor Moq of NSubstitute.


Antwoord 5, autoriteit 2%

Hier is een weergave van het geaccepteerde antwoord op F#, ik deed het gewoon voor mezelf en dacht dat het iemand tijd zou kunnen besparen. Ik heb ook het voorbeeld bijgewerkt zodat het overeenkomt met de bijgewerkte C#8 IAsyncEnumarable API en de Mock-setup aangepast om generiek te zijn.

   type TestAsyncEnumerator<'T> (inner : IEnumerator<'T> ) =     
        let inner : IEnumerator<'T> = inner
        interface IAsyncEnumerator<'T> with
            member this.Current with get() = inner.Current
            member this.MoveNextAsync () = ValueTask<bool>(Task.FromResult(inner.MoveNext()))
            member this.DisposeAsync () = ValueTask(Task.FromResult(inner.Dispose))
    type TestAsyncEnumerable<'T> =       
        inherit EnumerableQuery<'T>
        new (enumerable : IEnumerable<'T>) = 
            { inherit EnumerableQuery<'T> (enumerable) }
        new (expression : Expression) = 
            { inherit EnumerableQuery<'T> (expression) }
        interface IAsyncEnumerable<'T> with
            member this.GetAsyncEnumerator cancellationToken : IAsyncEnumerator<'T> =
                 new TestAsyncEnumerator<'T>(this.AsEnumerable().GetEnumerator())
                 :> IAsyncEnumerator<'T>
        interface IQueryable<'T> with
            member this.Provider with get() = new TestAsyncQueryProvider<'T>(this) :> IQueryProvider
    and 
        TestAsyncQueryProvider<'TEntity> 
        (inner : IQueryProvider) =       
        let inner : IQueryProvider = inner
        interface IAsyncQueryProvider with
            member this.Execute (expression : Expression) =
                inner.Execute expression
            member this.Execute<'TResult> (expression : Expression) =
                inner.Execute<'TResult> expression
            member this.ExecuteAsync<'TResult> ((expression : Expression), cancellationToken) =
                inner.Execute<'TResult> expression
            member this.CreateQuery (expression : Expression) =
                new TestAsyncEnumerable<'TEntity>(expression) :> IQueryable
            member this.CreateQuery<'TElement> (expression : Expression) =
                new TestAsyncEnumerable<'TElement>(expression) :> IQueryable<'TElement>
    let getQueryableMockDbSet<'T when 'T : not struct>
        (sourceList : 'T seq) : Mock<DbSet<'T>> =
        let queryable = sourceList.AsQueryable();
        let dbSet = new Mock<DbSet<'T>>()
        dbSet.As<IAsyncEnumerable<'T>>()
            .Setup(fun m -> m.GetAsyncEnumerator())
            .Returns(TestAsyncEnumerator<'T>(queryable.GetEnumerator())) |> ignore
        dbSet.As<IQueryable<'T>>()
            .SetupGet(fun m -> m.Provider)
            .Returns(TestAsyncQueryProvider<'T>(queryable.Provider)) |> ignore
        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.Expression).Returns(queryable.Expression) |> ignore
        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.ElementType).Returns(queryable.ElementType) |> ignore
        dbSet.As<IQueryable<'T>>().Setup(fun m -> m.GetEnumerator ()).Returns(queryable.GetEnumerator ()) |> ignore
        dbSet

Antwoord 6

Een veel eenvoudigere aanpak is om uw eigen ToListAsyncin een van de kernlagen te schrijven. U hebt geen concrete klassenimplementatie nodig. Iets als:

   public static async Task<List<T>> ToListAsync<T>(this IQueryable<T> queryable)
    {
        if (queryable is EnumerableQuery)
        {
            return queryable.ToList();
        }
        return await QueryableExtensions.ToListAsync(queryable);
    }

Dit heeft ook het extra voordeel dat u ToListAsync overal in uw app kunt gebruiken zonder dat u EF-referenties hoeft mee te slepen.


Antwoord 7

Gebruikmakend van het geaccepteerde antwoord van @Jed Veatch, evenals de opmerkingen van @Mandelbrotter, werkt de volgende oplossing voor .NET Core 3.1 en .NET 5. Hiermee wordt de uitzondering “Argumentexpressie is niet geldig” opgelost die voortvloeit uit het werken met de bovenstaande code in latere .NET-versies.

TL;DR – De volledige EnumerableExtensions.cs-code is hier.

Gebruik:

public static DbSet<T> GetQueryableAsyncMockDbSet<T>(List<T> sourceList) where T : class
{
    var mockAsyncDbSet = sourceList.ToAsyncDbSetMock<T>();
    var queryable = sourceList.AsQueryable();
    mockAsyncDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
    mockAsyncDbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));
    return mockAsyncDbSet.Object;
}

Vervolgens, met behulp van Moqen Autofixture, je kunt het volgende doen:

var myMockData = Fixture.CreateMany<MyMockEntity>();
MyDatabaseContext.SetupGet(x => x.MyDBSet).Returns(GetQueryableAsyncMockDbSet(myMockData));

Other episodes