如何用实体框架核心模拟异步存储库

本文关键字:异步 存储 模拟 核心 何用 实体 框架 | 更新日期: 2023-09-27 18:17:02

我正在尝试为调用异步存储库的类创建单元测试。我用的是ASP。. NET核心和实体框架核心。我的通用存储库是这样的。

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.

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

如何用实体框架核心模拟异步存储库

感谢@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);
}

尝试使用我的Moq/NSubstitute/FakeItEasy扩展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);

指出:

  • AutoMapper也支持从1.0.4 ver
  • 1.1.0版本支持DbQuery从3.0.0 ver开始支持EF Core 3.0
  • 。从5.0.0 ver
  • 开始支持Net 5

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

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

我正在维护两个开源项目,它们完成了设置模拟和实际模拟SaveChanges(Async)的繁重工作。

For EF Core: https://github.com/huysentruitw/entity-framework-core-mock

对于EF6: https://github.com/huysentruitw/entity-framework-mock

两个项目都有集成了Moq或NSubstitute的Nuget包

我对批准的解决方案有一些问题。显然,从实体框架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());
        }
    }
}

创建了这个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);

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

        MyRepository myRepository = new(mockContext.Object);
        Person? person = await myRepository.GetPersonById(6);

现在,我可以断言任何条件,没有问题。

Assert.NotNull(person);
Assert.True(person.Id == 6);
Assert.True(person.FirstName == "John");

这是一个被接受的f#答案的移植,我只是为自己做了这个,认为它可以节省别人的时间。我还更新了示例,以匹配更新的c# 8 iasyncenumable 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

一个更简单的方法是在一个核心层中编写自己的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引用。

我知道这个问题很老了,但是我找到了一个nuget包来做这个。

MockQueryable和MockQueryable。Moq

这为你做了所有的工作。

[TestCase("AnyFirstName", "AnyExistLastName", "01/20/2012", "Users with DateOfBirth more than limit")]
    [TestCase("ExistFirstName", "AnyExistLastName", "02/20/2012", "User with FirstName already exist")]
    [TestCase("AnyFirstName", "ExistLastName", "01/20/2012", "User already exist")]
    public void CreateUserIfNotExist(string firstName, string lastName, DateTime dateOfBirth, string expectedError)
    {
      //arrange
      var userRepository = new Mock<IUserRepository>();
      var service = new MyService(userRepository.Object);
      var users = new List<UserEntity>
      {
        new UserEntity {LastName = "ExistLastName", DateOfBirth = DateTime.Parse("01/20/2012", UsCultureInfo.DateTimeFormat)},
        new UserEntity {FirstName = "ExistFirstName"},
        new UserEntity {DateOfBirth = DateTime.Parse("01/20/2012", UsCultureInfo.DateTimeFormat)},
        new UserEntity {DateOfBirth = DateTime.Parse("01/20/2012", UsCultureInfo.DateTimeFormat)},
        new UserEntity {DateOfBirth = DateTime.Parse("01/20/2012", UsCultureInfo.DateTimeFormat)}
      };
      //expect
      var mock = users.BuildMock();
      userRepository.Setup(x => x.GetQueryable()).Returns(mock);
      //act
      var ex = Assert.ThrowsAsync<ApplicationException>(() =>
        service.CreateUserIfNotExist(firstName, lastName, dateOfBirth));
      //assert
      Assert.AreEqual(expectedError, ex.Message);
    }

参考@Jed Veatch的回答以及@Mandelbrotter的评论,下面的解决方案适用于。net Core 3.1和。net 5。这将解决"参数表达式无效"的问题。在以后的。net版本中使用上述代码会产生异常。

TL;DR -完整的EnumerableExtensions.cs代码在这里。

用法:

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));

对于每个坚持用异步查询,IAsyncQueryProvider和其他东西嘲笑DbContext的人。下面是netcore3.1及更高版本的复制-粘贴类型的示例用法。基于通用DbContextCreation和通用DbSet种子。

    public class MyDbContext : DbContext
    {
        public DbSet<MyEntity> MyEntities { get; set; }
    }
    public class MyEntity
    {
        public Guid Id { get; set; }
    }
    internal class MockDbContextAsynced<TDbContext>
    {
        private readonly TDbContext _mock;
        public TDbContext Object => _mock;
        public MockDbContextAsynced()
        {
            _mock = Activator.CreateInstance<TDbContext>();
        }
          // suppressed. see full code in source below
    }
    [Fact]
    public void Test()
    {
        var testData = new List<MyEntity>
        {
            new MyEntity() { Id = Guid.NewGuid() },
            new MyEntity() { Id = Guid.NewGuid() },
            new MyEntity() { Id = Guid.NewGuid() },
        };
        var mockDbContext = new MockDbContextAsynced<MyDbContext>();
        mockDbContext.AddDbSetData<MyEntity>(testData.AsQueryable());
        mockDbContext.MyEntities.ToArrayAsync();
        // or
        mockDbContext.MyEntities.SingleAsync();
        // or etc.
        
        // To inject MyDbContext as type parameter with mocked data
        var mockService = new SomeService(mockDbContext.Object);
    }

有关完整实现的类型,请参阅此源代码:https://gist.github.com/Zefirrat/a04658c827ba3ebffe03fda48d53ea11