如何使用Entity Framework Core模拟异步存储库

Jed*_*tch 61 c# unit-testing moq entity-framework-core asp.net-core

我正在尝试为调用异步存储库的类创建单元测试.我正在使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我有一个服务类,它在存储库的实例上调用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;
    }
Run Code Online (Sandbox Code Playgroud)

我正在尝试在下面的测试中模拟存储库调用:

    [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);
    }
Run Code Online (Sandbox Code Playgroud)

但是,当测试执行对存储库的调用时,我收到以下错误:

The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IEntityQueryProvider can be used for Entity Framework asynchronous operations.
Run Code Online (Sandbox Code Playgroud)

如何正确模拟存储库以使其工作?

Jed*_*tch 87

感谢@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());
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我使用这些类的更新测试用例:

[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);
}
Run Code Online (Sandbox Code Playgroud)

非常感谢你的帮助!

  • 您愿意将其更新到 core 3.0 吗?我无法在 _inner.Execute&lt;TResults&gt;(expression) 上传递“参数表达式无效” (8认同)
  • 检查[here](http://stackoverflow.com/a/40500030/5233410)给出的答案,该答案引用了使用扩展方法的答案.快乐的编码! (2认同)
  • 公共 TResult ExecuteAsync&lt;TResult&gt;(表达式表达式,CancellationToken 取消令牌){ 对象 returnValue = 执行(表达式); 返回 ConvertToThreadingTResult&lt;TResult&gt;(returnValue); } (2认同)
  • TResult IAsyncQueryProvider.ExecuteAsync&lt;TResult&gt;(Expression 表达式, CancellationToken CancellationToken) { var returnValue = ExecuteAsync&lt;TResult&gt;(表达式, 默认值); 返回 ConvertToTResult&lt;TResult&gt;(returnValue); } (2认同)
  • 私有静态 TR ConvertToTResult &lt;TR&gt;(dynamic toConvert) { return (TR)toConvert; } (2认同)
  • 私有静态 TR ConvertToThreadingTResult&lt;TR&gt;(dynamic toConvert) { return (TR)Task.FromResult(toConvert); } (2认同)
  • IAsyncEnumerable 不包含 GetEnumerator,.NET 5.0 中仅包含 GetAsyncEnumerator。这些解决方案均不适用于 .NET 5.0 (2认同)

R.T*_*tov 23

尝试使用我的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);
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

注意:

  • 1.0.4 ver也支持AutoMapper
  • 从1.1.0版本支持DbQuery

  • 这似乎是一个非常好的包,而不是为假的`IAsyncQueryProvider`等编写一堆双重实现. (6认同)

Dea*_*tin 8

更少的代码解决方案.使用内存中的db上下文,它应该为您启动所有集合的引导.您不再需要在上下文中模拟DbSet,但是如果您想从服务返回数据,则可以简单地返回内存上下文的实际设置数据.

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

  _db = new SaasDispatcherDbContext(optionsBuilder: options);
Run Code Online (Sandbox Code Playgroud)


Dar*_*ode 7

我对批准的解决方案有一些问题。显然,从 Entity Framework 5.0.3 开始发生了变化。IAsyncQueryProvider、IAsyncEnumerable 和 IAsyncEnumerator 具有必须实现的不同方法。我在网上找到一篇文章,提供了解决方案。这适用于我的 .NET 6 应用程序。请务必包含该using Microsoft.EntityFrameworkCore.Query声明。对于我来说,Visual Studio 无法找到这三个界面,并希望我手动创建它们。

using Microsoft.EntityFrameworkCore.Query;
using System.Linq.Expressions;
namespace MyApp.Tests
{
    internal class AsyncHelper
    {
        internal class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider
        {
            private readonly IQueryProvider _innerQueryProvider;

            internal TestAsyncQueryProvider(IQueryProvider inner)
            {
                _innerQueryProvider = 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) => _innerQueryProvider.Execute(expression);

            public TResult Execute<TResult>(Expression expression) => _innerQueryProvider.Execute<TResult>(expression);

            TResult IAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
            {
                Type expectedResultType = typeof(TResult).GetGenericArguments()[0];
                object? executionResult = ((IQueryProvider)this).Execute(expression);

                return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult))
                    .MakeGenericMethod(expectedResultType)
                    .Invoke(null, new[] { executionResult });
            }
        }

        internal class TestAsyncEnumerable<T> : EnumerableQuery<T>, IAsyncEnumerable<T>, IQueryable<T>
        {
            public TestAsyncEnumerable(IEnumerable<T> enumerable)
                : base(enumerable)
            { }

            public TestAsyncEnumerable(Expression expression)
                : base(expression)
            { }

            IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider<T>(this);

            public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = new CancellationToken())
                => new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
        }

        internal class TestAsyncEnumerator<T> : IAsyncEnumerator<T>
        {
            private readonly IEnumerator<T> _enumerator;

            public TestAsyncEnumerator(IEnumerator<T> inner)
            {
                _enumerator = inner;
            }

            public T Current => _enumerator.Current;

            public ValueTask DisposeAsync() => new(Task.Run(() => _enumerator.Dispose()));

            public ValueTask<bool> MoveNextAsync() => new(_enumerator.MoveNext());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

创建此 AsyncHelper 后,我能够模拟我的数据库上下文。

IQueryable<MyEntity> myList = new List<MyEntity>
        {
            new()
            {
                Id= 6,
                FirstName = "John",
                MidName = "Q",
                LastName = "Doe",
            }
        }.AsQueryable();


        Mock<DbSet<MyEntity>> dbSetMock = new();

        dbSetMock.As<IAsyncEnumerable<MyEntity>>()
            .Setup(m => m.GetAsyncEnumerator(default))
            .Returns(new AsyncHelper.TestAsyncEnumerator<MyEntity>(myList.GetEnumerator()));

        dbSetMock.As<IQueryable<MyEntity>>()
            .Setup(m => m.Provider)
            .Returns(new AsyncHelper.TestAsyncQueryProvider<MyEntity>(myList.Provider));

        dbSetMock.As<IQueryable<MyEntity>>().Setup(m => m.Expression)
            .Returns(myList.Expression);

        dbSetMock.As<IQueryable<MyEntity>>().Setup(m => m.ElementType)
            .Returns(myList.ElementType);

        dbSetMock.As<IQueryable<MyEntity>>().Setup(m => m.GetEnumerator())
            .Returns(() => myList.GetEnumerator());

Mock<MyDbContext> mockContext = new();
mockContext.Setup(c => c.People).Returns(dbSetMock().Object);
Run Code Online (Sandbox Code Playgroud)

然后,我用模拟上下文安排单元测试。

        MyRepository myRepository = new(mockContext.Object);
        Person? person = await myRepository.GetPersonById(6);
Run Code Online (Sandbox Code Playgroud)

现在,我可以毫无问题地断言任何条件。

Assert.NotNull(person);
Assert.True(person.Id == 6);
Assert.True(person.FirstName == "John");
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助。