如何使用 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&lt;T&gt;IAsyncEnumerableAccessor&lt;T&gt; 接口 【参考方案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(expression) 上通过“参数表达式无效” public TResult ExecuteAsync(表达式表达式,CancellationToken cancelToken) object returnValue = Execute(expression);返回 ConvertToThreadingTResult(returnValue); TResult IAsyncQueryProvider.ExecuteAsync(表达式表达式,CancellationToken cancelToken) var returnValue = ExecuteAsync(表达式,默认值);返回 ConvertToTResult(returnValue); 私有静态 TR ConvertToTResult (动态 toConvert) return (TR)toConvert; 【参考方案2】:

尝试使用我的 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?

如何使用 Entity Framework Core 获取主键值

如何在 Entity Framework 6 中使用 DbDataReader 获取表名?