So verspotten Sie ein asynchrones Repository mit Entity Framework Core

75

Ich versuche, einen Komponententest für eine Klasse zu erstellen, die ein asynchrones Repository aufruft. Ich verwende ASP.NET Core und Entity Framework Core. Mein generisches Repository sieht so aus.

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

Dann habe ich eine Serviceklasse, die FindBy und FirstOrDefaultAsync für eine Instanz des Repositorys aufruft:

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

Ich versuche, den Repository-Aufruf in meinem Test unten zu verspotten:

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

Wenn der Test den Aufruf des Repositorys ausführt, wird jedoch die folgende Fehlermeldung angezeigt:

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

Wie kann ich das Repository richtig verspotten, damit dies funktioniert?

Jed Veatch
quelle
2
Möglicherweise
Nkosi
Lesen Sie den Abschnitt überTesting with async queries
Nkosi
Sie müssen beide IQueryable<T>und IAsyncEnumerableAccessor<T>Schnittstellen auch verspotten
Dealdiane

Antworten:

97

Vielen Dank an @Nkosi, der mich auf einen Link mit einem Beispiel für das Gleiche in EF 6 verwiesen hat: https://msdn.microsoft.com/en-us/library/dn314429.aspx . Dies funktionierte nicht genau so wie es ist mit EF Core, aber ich konnte damit beginnen und Änderungen vornehmen, damit es funktioniert. Unten sind die Testklassen aufgeführt, die ich erstellt habe, um IAsyncQueryProvider zu "verspotten":

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

Und hier ist mein aktualisierter Testfall, der diese Klassen verwendet:

[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);
}
Jed Veatch
quelle
Ich bin froh, dass du es irgendwann herausgefunden hast. Ich habe den Quellcode auf Github durchsucht, um zu sehen, ob sie Moq-Beispiele hatten. Komischerweise habe ich einen untersucht, als ich nachgesehen habe, dass Sie zu Ihrer eigenen Lösung gekommen sind. Cool.
Nkosi
Ich würde das in eine Erweiterungsmethode konvertieren, damit Sie es in Ihren Tests wiederverwenden können.
Nkosi
2
Überprüfen Sie die hier angegebene Antwort, die auf diese Antwort verweist, bei der die Erweiterungsmethode verwendet wurde. Viel Spaß beim Codieren !!!
Nkosi
Nochmals vielen Dank, @Nkosi!
Jed Veatch
6
Wären Sie bereit, dies auf Core 3.0 zu aktualisieren? Ich kann nicht übergeben werden "Argument Ausdruck ist ungültig" auf _inner.Execute <TResults> (Ausdruck)
BillRob
44

Versuchen Sie, meine Moq / NSubstitute / FakeItEasy-Erweiterung MockQueryable zu verwenden : Unterstützt alle Sync / Async-Vorgänge (weitere Beispiele 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 wird ebenfalls unterstützt

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

Anmerkungen:

  • AutoMapper wird auch ab Version 1.0.4 unterstützt
  • DbQuery wird ab Version 1.1.0 unterstützt
  • EF Core 3.0 wird ab Version 3.0.0 unterstützt
  • .Net 5 unterstützt ab 5.0.0 ver
R. Titov
quelle
7
Dies scheint ein wirklich schönes Paket zu sein, anstatt eine Reihe von doppelten Implementierungen für eine Fälschung IAsyncQueryProviderusw. zu
schreiben
1
Dies ist ein großartiges Paket, das den Trick sofort gemacht hat. Vielen Dank!
Sigge
1
@Noob versuchen, Link aus meinem Kommentar zu öffnen, oder werfen Sie einen Blick auf github.com/romantitov/MockQueryable/blob/master/src/…
R.Titov
1
@Noob in MyService.CreateUserIfNotExist Ich verwende FirstOrDefaultAsync, und hier sehen Sie Tests für die CreateUserIfNotExist-Logik github.com/romantitov/MockQueryable/blob/master/src/…
R.Titov
1
@ R.Titov danke, ich habe meinen Fehler gefunden, erstens habe ich meine Daten, die ich zurückgeben sollte, nicht verspottet und zweitens hat das Erstellen des Verspottens in return () auch einen anderen Fehler verursacht. Wie auch immer, jetzt funktioniert es, danke !!
Noob
8

Viel weniger Codelösung. Verwenden Sie den speicherinternen Datenbankkontext, der dafür sorgen soll, dass alle Sets für Sie gebootet werden. Sie müssen das DbSet in Ihrem Kontext nicht mehr verspotten, aber wenn Sie beispielsweise Daten von einem Dienst zurückgeben möchten, können Sie einfach die tatsächlich festgelegten Daten des speicherinternen Kontexts zurückgeben.

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

  _db = new SaasDispatcherDbContext(optionsBuilder: options);
Dean Martin
quelle
Dies scheint eine gute Lösung zu sein. Warum nicht? Fügen Sie einen Verweis auf das speicherinterne Nuget-Paket hinzu, um die UseInMemoryDatabase-Erweiterungsmethode abzurufen. docs.microsoft.com/en-us/ef/core/miscellaneous/testing/…
Andrew
Wäre dies nicht eine Form des Integrationstests? Ich weiß, dass es sich um eine In-Memory-Datenbank handelt, aber Sie werden gegen ein System testen, wie es Integrationstests sind. Korrigieren Sie mich, wenn ich falsch liege.
Elfico
2

Hier ist ein Port der akzeptierten Antwort auf F #. Ich habe es nur für mich getan und dachte, es könnte jemandem Zeit sparen. Ich habe das Beispiel auch aktualisiert, um es mit der aktualisierten C # 8 IAsyncEnumarable-API abzugleichen, und das Mock-Setup so angepasst, dass es generisch ist.

    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
Ryan
quelle
Möglicherweise möchten Sie eine neue Frage stellen, die sich auf F # bezieht, und mit einer beantworten. Es gehört nicht hierher.
ilkerkaran
3
Die Frage erwähnt nicht die Sprache. Ich habe auch die akzeptierte Antwort verbessert, indem ich mich an die neueste API gehalten habe. Ich denke nicht, dass dies einen Abschlag verdient, da es nützlich ist. Ich füge zusätzliche Informationen für .Net-Programmierer hinzu, die hier landen.
Ryan
2
Es enthält C # -Tag
ilkerkaran
1
So oder so, Mann, ich verletze niemanden oder trübe das Thema, ich füge weitere nützliche Informationen hinzu, die direkt mit der Frage zusammenhängen. Ich denke, du warst unfair, aber das ist dein Recht, denke ich.
Ryan