如何使用 Entity Framework Core 模拟异步存储库
Posted
技术标签:
【中文标题】如何使用 Entity Framework Core 模拟异步存储库【英文标题】:How to mock an async repository with Entity Framework Core 【发布时间】:2017-03-21 10:46:36 【问题描述】:我正在尝试为调用异步存储库的类创建单元测试。我正在使用 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 = $"-sloginToken.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.
如何正确模拟存储库以使其正常工作?
【问题讨论】:
这可能对msdn.microsoft.com/en-us/library/dn314429.aspx有帮助 阅读Testing with async queries
上的部分
您还需要模拟IQueryable<T>
和IAsyncEnumerableAccessor<T>
接口
【参考方案1】:
感谢@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);
【讨论】:
检查给出的答案here,它引用了使用扩展方法的这个答案。编码快乐!!! 您愿意将此更新到核心 3.0 吗?我无法在 _inner.Execute尝试使用我的 Moq/NSubstitute/FakeItEasy 扩展 MockQueryable: 支持所有同步/异步操作(见更多示例here)
//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);
注意事项:
从 1.0.4 版本开始也支持 AutoMapper 从 1.1.0 版本开始支持 DbQuery 从 3.0.0 版本开始支持 EF Core 3.0 .Net 5 supported 来自 5.0.0 版本【讨论】:
这似乎是一个非常好的包,而不是为伪造的IAsyncQueryProvider
等编写一堆双重实现。
这是一个很棒的包,它马上就成功了。谢谢!
我没有得到的是第一个块中 _userRepository 的来源???或者 TestDbSetRepository 来自哪里??
@Noob 尝试打开我评论中的链接,或者看看github.com/romantitov/MockQueryable/blob/master/src/…
@Noob in MyService.CreateUserIfNotExist 我使用 FirstOrDefaultAsync,在这里你可以看到 CreateUserIfNotExist 逻辑的测试github.com/romantitov/MockQueryable/blob/master/src/…【参考方案3】:
更少的代码解决方案。使用内存中的数据库上下文,它应该负责为您引导所有集合。您不再需要在您的上下文中模拟 DbSet,但是如果您想从服务返回数据,您可以简单地返回内存中上下文的实际设置数据。
DbContextOptions< SaasDispatcherDbContext > options = new DbContextOptionsBuilder< SaasDispatcherDbContext >()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
_db = new SaasDispatcherDbContext(optionsBuilder: options);
【讨论】:
这似乎是一个很好的解决方案。为什么不?添加对内存中 nuget 包的引用以获取 UseInMemoryDatabase 扩展方法。 docs.microsoft.com/en-us/ef/core/miscellaneous/testing/… 这不是集成测试的一种形式吗?我知道这是一个内存数据库,但是您将针对集成测试的系统进行测试。如果我错了,请纠正我。 集成测试应该测试相互集成的系统。由于您没有离开原始系统的上下文,或者更确切地说,通过将外部系统替换为 in-memory-mock 系统来“模拟”外部系统,因此您并没有真正离开 unittest-context。【参考方案4】:我正在维护两个开源项目,它们负责设置模拟并实际模拟 SaveChanges(Async)
。
对于 EF Core:https://github.com/huysentruitw/entity-framework-core-mock
对于 EF6:https://github.com/huysentruitw/entity-framework-mock
两个项目都有集成了 Moq 或 NSubstitute 的 Nuget 包。
【讨论】:
【参考方案5】:这是 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
【讨论】:
您可能想提出一个涉及 F# 的新问题并回答一个问题。它不属于这里。 问题没有提到语言。我还通过坚持最新的 API 改进了公认的答案。我认为这不值得降价,因为它很有用。我正在为登陆这里的 .Net 程序员添加额外信息。 它包含 C# 标签 无论哪种方式,我都没有伤害任何人或混淆主题,我正在添加更多与问题直接相关的有用信息。我认为你不公平,但我猜这是你的要求。【参考方案6】:一种更简单的方法是在其中一个核心层中编写您自己的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 引用。
【讨论】:
【参考方案7】:利用@Jed Veatch 接受的答案以及@Mandelbrotter 提供的 cmets,以下解决方案适用于 .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));
【讨论】:
以上是关于如何使用 Entity Framework Core 模拟异步存储库的主要内容,如果未能解决你的问题,请参考以下文章
Entity Framework 学习系列 - 认识理解Entity Framework
如何使用 Entity Framework Core 模拟异步存储库
如何使用 Entity Framework 生成和自动递增 Id
如何使用 Entity Framework Core 正确保存 DateTime?