我正在尝试为调用异步存储库的类创建单元测试。我正在使用ASP.NET Core和Entity Framework Core。我的通用存储库看起来像这样。
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();
}
}
然后我有一个服务类,它在存储库的实例上调用FindBy和FirstOrDefaultAsync:
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;
}
我正在尝试在下面的测试中模拟存储库调用:
[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);
}
但是,当测试执行对存储库的调用时,我收到以下错误:
The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IEntityQueryProvider can be used for Entity Framework asynchronous operations.
如何正确模拟存储库以使其正常工作?
答案 0 :(得分:72)
感谢@Nkosi指出我在EF 6中做同样事情的例子的链接:https://msdn.microsoft.com/en-us/library/dn314429.aspx。这与EF Core完全不同,但我能够从它开始并进行修改以使其正常工作。下面是我为“模拟”IAsyncQueryProvider而创建的测试类:
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());
}
}
这是我使用这些类的更新测试用例:
[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);
}
非常感谢你的帮助!
答案 1 :(得分:18)
尝试使用我的Moq / NSubstitute扩展MockQueryable:https://github.com/romantitov/MockQueryable 支持所有同步/异步操作
//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
//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);
注意:
答案 2 :(得分:8)
更少的代码解决方案。使用内存中的db上下文,它应该为您启动所有集合的引导。您不再需要在上下文中模拟DbSet,但是如果您想从服务返回数据,则可以简单地返回内存上下文的实际设置数据。
DbContextOptions< SaasDispatcherDbContext > options = new DbContextOptionsBuilder< SaasDispatcherDbContext >()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new SaasDispatcherDbContext(optionsBuilder: options);
答案 3 :(得分:2)
以下是F#公认答案的一部分,我只是为自己做的,并认为它可以节省某人的时间。我还更新了示例,以匹配更新的C#8 IAsyncEnumarable API,并调整了Mock设置以使其通用。
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
答案 4 :(得分:0)
我维护着两个开源项目,这些项目繁重地设置了模拟并实际上模拟了SaveChanges(Async)
。
对于EF Core:https://github.com/huysentruitw/entity-framework-core-mock
对于EF6:https://github.com/huysentruitw/entity-framework-mock
两个项目都具有与Moq或NSubstitute集成的Nuget软件包。
答案 5 :(得分:0)
一种更简单的方法是在其中一个核心层中编写您自己的 ToListAsync
。您不需要任何具体的类实现。类似的东西:
public static async Task<List<T>> ToListAsync<T>(this IQueryable<T> queryable)
{
if (queryable is EnumerableQuery)
{
return queryable.ToList();
}
return await QueryableExtensions.ToListAsync(queryable);
}
这还有一个额外的好处,即您可以从应用程序的任何位置使用 ToListAsync,而无需一直拖动 EF 引用。
答案 6 :(得分:0)
利用@Jed Veatch 接受的答案以及@Mandelbrotter 提供的评论,以下解决方案适用于 .NET Core 3.1 和 .NET 5。这将解决工作中出现的“参数表达式无效”异常在以后的 .NET 版本中使用上述代码。
TL;DR - 完整的 EnumerableExtensions.cs 代码为 here。
用法:
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;
}
然后,使用 Moq 和 Autofixture,您可以:
var myMockData = Fixture.CreateMany<MyMockEntity>();
MyDatabaseContext.SetupGet(x => x.MyDBSet).Returns(GetQueryableAsyncMockDbSet(myMockData));