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?
c#
unit-testing
asp.net-core
moq
entity-framework-core
Jed Veatch
quelle
quelle
Testing with async queries
IQueryable<T>
undIAsyncEnumerableAccessor<T>
Schnittstellen auch verspottenAntworten:
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); }
quelle
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:
quelle
IAsyncQueryProvider
usw. zuViel 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);
quelle
Ich pflege zwei Open-Source-Projekte, die das Einrichten der Mocks und das tatsächliche Emulieren schwer machen
SaveChanges(Async)
.Für EF Core: https://github.com/huysentruitw/entity-framework-core-mock
Für EF6: https://github.com/huysentruitw/entity-framework-mock
Beide Projekte haben Nuget-Pakete mit Integration für Moq oder NSubstitute.
quelle
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
quelle