我对使用moq进行测试相当新,而且我有一个奇怪的问题(至少对我来说这似乎很奇怪),但我可能只是没有正确设置模拟对象。我有一个存储库层,它使用EntityFrameworkCore来处理我的DbContext。存储库中的一个特定功能允许我返回一个已排序的列表,而不会将Linq或EFCore函数暴露给调用该函数的服务层。
假设我有一个这样的Model类:
public class SomeClass {
public string Foo { get; set; }
}
我的DbContext中有一个名为someClasses的DbSet。我用来对某些类进行排序的我的存储库中的三个函数是:
public async Task<List<SomeClass>> GetSomeClassesAsync(string orderBy = "", bool descending = false) {
var returnVals = _context.someClasses.AsQueryable();
returnVals = SortQueryableCollectionByProperty(returnVals, orderBy, descending);
return await returnVals.ToListAsync();
}
private IQueryable<T> SortQueryableCollectionByProperty<T>(IQueryable<T> queryable, string propertyName, bool descending) where T : class {
if (typeof(T).GetProperty(propertyName) != null) {
if (descending) {
queryable = queryable.OrderByDescending(q => GetPropertyValue(propertyName, q));
} else {
queryable = queryable.OrderBy(q => GetPropertyValue(propertyName, q));
}
}
return queryable;
}
private object GetPropertyValue<T>(string propertyName, T obj) {
return obj.GetType().GetProperty(propertyName).GetAccessors()[0].Invoke(obj, null);
}
所以我对GetSomeClassesAsync()进行了2次单元测试。第一个单元测试确保返回的列表由Foo排序,第二个单元测试在尝试按Bar排序(不存在的属性)时返回无序列表。以下是我的测试设置方式:
private Mock<DbContext> mockContext;
private MyRepository repo;
[TestInitialize]
public void InitializeTestData() {
mockContext = new Mock<DbContext>();
repo = new MyRepository(mockContext.Object);
}
[TestMethod]
public async Task GetSomeClassesAsync_returns_ordered_list() {
var data = new List<SomeClass> {
new SomeClass { Foo = "ZZZ" },
new SomeClass { Foo = "AAA" },
new SomeClass { Foo = "CCC" }
};
var mockSomeClassDbSet = DbSetMocking.CreateMockSet(new TestAsyncEnumerable<SomeClass>(data));
mockContext.Setup(m => m.someClasses).Returns(mockSomeClassDbSet.Object);
var sortedResults = await repo.GetSomeClassesAsync(nameof(SomeClass.Foo));
Assert.AreEqual("AAA", sortedResults[0].Foo);
Assert.AreEqual("CCC", sortedResults[1].Foo);
Assert.AreEqual("ZZZ", sortedResults[2].Foo);
}
[TestMethod]
public async Task GetSomeClassesAsync_returns_unordered_list() {
var data = new List<SomeClass> {
new SomeClass { Foo = "ZZZ" },
new SomeClass { Foo = "AAA" },
new SomeClass { Foo = "CCC" }
};
var mockSomeClassDbSet = DbSetMocking.CreateMockSet(new TestAsyncEnumerable<SomeClass>(data));
mockContext.Setup(m => m.someClasses).Returns(mockSomeClassDbSet.Object);
var unsortedResults = await repo.GetSomeClassesAsync("Bar");
Assert.AreEqual("ZZZ", unsortedResults[0].Foo);
Assert.AreEqual("AAA", unsortedResults[1].Foo);
Assert.AreEqual("CCC", unsortedResults[2].Foo);
}
DbSetMocking.CreateMockSet()取自here,而TestAsyncEnumerable取自here
我反击的是第一个返回有序列表的测试。一切正常。第二次测试失败,我收到此错误消息:
System.NotImplementedException:未实现方法或操作。
当代码到达ToListAsync()时会抛出此异常。我没有得到的是为什么当它经历排序然后调用ToListAsync()时没有错误发生,但是当跳过排序并且调用ToListAsync()时,抛出该异常。我没有正确设置我的模拟对象吗?
答案 0 :(得分:2)
MosquitoBite 的答案是正确的。但是对于 .Net 5,IAsyncQueryProvider 接口中的 ExecuteAsync 方法发生了一些变化:
TResult IAsyncQueryProvider.ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
{
return Execute<TResult>(expression);
}
对于 IAsyncEnumerator,DisposeAsync 和 MoveNextAsync 方法也发生了变化:
public ValueTask DisposeAsync()
{
_inner.Dispose();
return ValueTask.CompletedTask;
}
public ValueTask<bool> MoveNextAsync()
{
return ValueTask.FromResult(_inner.MoveNext());
}
答案 1 :(得分:0)
简短的回答是,使用Moq进行的设置中没有任何东西导致引发NotImplementedException。需要设置Ef Provider设置以支持异步方法。
以下是最详尽的答案。 :) 我研究了在测试使用EFCore上下文的异步方法时使用哪种方法。由于setup used with Entity Framework 6上的文档确实非常好,但是the documentation for EFCore专注于InMemoryProvider和SQLite-InMemory-Mode,并且不包括用于异步测试的文档,甚至没有暗示它甚至不是,这并不是很明显。支持的。或更准确地说,我没有找到任何东西。
因此,到目前为止,我发现与EFCore配合使用的解决方案如下:
或者,如果您想节省时间,请在下面复制并粘贴此代码。 :) 我只是想告诉您我是如何到达那里的,因为这可能不是一成不变的解决方案。但是,除非有一个标准的解决方案(或者至少在我找到它之前),否则这似乎是可行的方法。
public static class DbSetMockSetup
{
public static Mock<DbSet<T>> SetupMockDbSet<T>(IEnumerable<T> dataToBeReturnedOnGet) where T : class
{
var mocks = dataToBeReturnedOnGet.AsQueryable();
var mockSet = new Mock<DbSet<T>>();
mockSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider<T>(mocks.Provider));
mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(mocks.Expression);
mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(mocks.ElementType);
mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(mocks.GetEnumerator());
mockSet.As<IAsyncEnumerable<T>>()
.Setup(x => x.GetEnumerator())
.Returns(new TestAsyncEnumerator<T>(mocks.GetEnumerator()));
return mockSet;
}
}
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> GetAsyncEnumerator()
{
return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
}
IQueryProvider IQueryable.Provider
{
get { return new TestAsyncQueryProvider<T>(this); }
}
public IAsyncEnumerator<T> GetEnumerator()
{
return GetAsyncEnumerator();
}
}
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());
}
}
然后您可以使用类似()的设置:
public async Task Create_ReturnsModelWithANonEmptyListOfProducts()
{
var dbSetOfFoos = DbSetMockSetup.SetupMockDbSet(new List<Foo> { new Foo{ ... }});
_context.Reset(); // _context is a Mock<MyContext>
_context.Setup(db => db.Foos).Returns(dbSetOfFoos.Object);
var sut = new ProductListViewModelFactory(_context.Object);
var model = await sut.CreateAsync();
// assert
...
}
我希望这会帮助您解决问题。祝你好运!