簡體   English   中英

如何使用 Entity Framework Core 模擬異步存儲庫

[英]How to mock an async repository with Entity Framework Core

我正在嘗試為調用異步存儲庫的 class 創建一個單元測試。 我正在使用 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();
    }
}

然后我有一個服務 class 在存儲庫的實例上調用 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);

筆記:

  • 從 1.0.4 版本開始也支持 AutoMapper
  • 從 1.1.0 版本開始支持 DbQuery
  • 從 3.0.0 版本開始支持 EF Core 3.0
  • 從 5.0.0 版本開始支持.Net 5

少得多的代碼解決方案。 使用內存數據庫上下文,它應該負責為您引導所有集合。 您不再需要在您的上下文中模擬 DbSet,但是例如,如果您想從服務返回數據,您可以簡單地返回內存中上下文的實際設置數據。

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

  _db = new SaasDispatcherDbContext(optionsBuilder: options);

我正在維護兩個開源項目,它們負責設置模擬並實際模擬SaveChanges(Async)

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

對於 EF6: https : //github.com/huysentruitw/entity-framework-mock

這兩個項目都有與 Moq 或 NSubstitute 集成的 Nuget 包。

這是 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

一種更簡單的方法是在核心層之一中編寫自己的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 引用。

利用@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;
}

然后,使用MoqAutofixture ,您可以執行以下操作:

var myMockData = Fixture.CreateMany<MyMockEntity>();
MyDatabaseContext.SetupGet(x => x.MyDBSet).Returns(GetQueryableAsyncMockDbSet(myMockData));

對於所有在 mocking DbContext中使用異步查詢、 IAsyncQueryProvider和其他內容的人。 下面是 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

我知道這個問題很老,但我找到了 nuget package 來做到這一點。

MockQueryableMockQueryable.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);
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM