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

我试图为调用异步存储库的类创建一个单元测试。我使用的是 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.

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

57300 次浏览

Thanks to @Nkosi for pointing me to a link with an example of doing the same thing in EF 6: https://msdn.microsoft.com/en-us/library/dn314429.aspx. This didn't work exactly as-is with EF Core, but I was able to start with it and make modifications to get it working. Below are the test classes that I created to "mock" 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());
}
}

And here is my updated test case that uses these classes:

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

Try to use my Moq/NSubstitute/FakeItEasy extension MockQueryable: supported all Sync/Async operations (see more examples 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 also supported

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

Notes:

  • AutoMapper is also supported from 1.0.4 ver
  • DbQuery supported from 1.1.0 ver
  • EF Core 3.0 supported from 3.0.0 ver
  • .Net 5 supported from 5.0.0 ver

Much less code solution. Use the in-memory db context which should take care of bootstrapping all the sets for you. You no longer need to mock out the DbSet on your context but if you want to return data from a service for example, you can simply return the actual set data of the in-memory context.

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


_db = new SaasDispatcherDbContext(optionsBuilder: options);

I'm maintaining two open-source projects that do the heavy lifting of setting up the mocks and actually emulates SaveChanges(Async).

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

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

Both projects have Nuget packages with integration for Moq or NSubstitute.

Here is a port of the accepted answer to F#, I just did it for myself and thought it may save someone the time. I have also updated the example to match the updated C#8 IAsyncEnumarable API and tweaked the Mock setup to be generic.

    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

A way simpler approach is to write your own ToListAsync in one of the core layers. You dont need any concrete class implementation. Something like:

    public static async Task<List<T>> ToListAsync<T>(this IQueryable<T> queryable)
{
if (queryable is EnumerableQuery)
{
return queryable.ToList();
}


return await QueryableExtensions.ToListAsync(queryable);
}

This also has the added benefit that you could use ToListAsync from anywhere in your app without needing to drag EF references all along.

Leveraging @Jed Veatch's accepted answer, as well as the comments provided by @Mandelbrotter, the following solution works for .NET Core 3.1 and .NET 5. This will resolve the "Argument expression is not valid" exception that arises from working with the above code in later .NET versions.

TL;DR - Complete EnumerableExtensions.cs code is here.

Usage:

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

Then, using Moq and Autofixture, you can do:

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

For everyone who stuck at mocking DbContext with async queries, IAsyncQueryProvider and other things. Heres example usage of copy-paste types for netcore3.1 and higher. Based on generic DbContextCreation and generic DbSet seed.

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


For full implemented types see this source: https://gist.github.com/Zefirrat/a04658c827ba3ebffe03fda48d53ea11

I know this question is old, but I found a nuget package to do this.

MockQueryable and MockQueryable.Moq

This does all of the work for you.

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