含TransactionScope的EF Core异步查询NSubstitute单元测试报错
我正在编写仓储方法的单元测试,该方法通过自定义异步扩展方法查询EF Core的DbSet,此方法将FirstOrDefaultAsync()包装在ReadUncommitted隔离级别的TransactionScope中。
仓储方法代码
return await _context.Entities .Where(e => e.Id == someId) .CustomFirstOrDefaultAsync();
其中CustomFirstOrDefaultAsync()是将FirstOrDefaultAsync()用TransactionScope包装的扩展方法。
测试设置
使用NSubstitute模拟DbSet,配置为返回支持异步LINQ的可查询模拟对象,基于内存中的实体列表创建:
var testData = new List<Entity> { /* 测试实体 */ }; var mockDbSet = testData.AsQueryable().GetQueryableMockDbSet(); mockContext.Entities.Returns(mockDbSet);
异常信息
运行测试时抛出NSubstitute异常:
Test method Data.Tests.RepositoriesTests.RepositoryTest.GetDetailsById_ShouldReturnCorrectDetails threw exception:
NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException: Could not find a call to return from.Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)), and that you are not configuring other substitutes within Returns() (for example, avoid this: mySub.SomeMethod().Returns(ConfigOtherSub())).
If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member.
Return values cannot be configured for non-virtual/non-abstract members.Correct use:
mySub.SomeMethod().Returns(returnValue);Potentially problematic use:
mySub.SomeMethod().Returns(ConfigOtherSub());Instead try:
var returnValue = ConfigOtherSub();
mySub.SomeMethod().Returns(returnValue);
异常指向仓储方法中调用扩展方法执行查询的代码行。
自定义扩展方法实现
public static async Task<T> CustomFirstOrDefaultAsync<T>(this IQueryable<T> query) { T result; using (var scope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel = IsolationLevel.ReadUncommitted }, asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled)) { result = await query.FirstOrDefaultAsync(); scope.Complete(); } return result; }
DbSet模拟扩展类实现
public static class DbSetExtensions { public static DbSet<T> GetQueryableMockDbSet<T>(this IEnumerable<T> sourceList) where T : class { var queryable = sourceList.AsQueryable(); var dbSet = Substitute.For<DbSet<T>, IQueryable<T>, IAsyncEnumerable<T>>(); ((IQueryable<T>)dbSet).Provider.Returns(new TestAsyncQueryProvider<T>(queryable.Provider)); ((IQueryable<T>)dbSet).Expression.Returns(queryable.Expression); ((IQueryable<T>)dbSet).ElementType.Returns(queryable.ElementType); ((IQueryable<T>)dbSet).GetEnumerator().Returns(queryable.GetEnumerator()); ((IAsyncEnumerable<T>)dbSet).GetAsyncEnumerator().Returns(new TestAsyncEnumerator<T>(queryable.GetEnumerator())); return dbSet; } public class TestAsyncQueryProvider<TEntity> : IAsyncQueryProvider { private readonly IQueryProvider _inner; public 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 TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { var expectedResultType = typeof(TResult).GetGenericArguments()[0]; var executionResult = typeof(IQueryProvider) .GetMethod( name: nameof(IQueryProvider.Execute), genericParameterCount: 1, types: new[] { typeof(Expression) }) .MakeGenericMethod(expectedResultType) .Invoke(this, new[] { expression }); return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult)) ?.MakeGenericMethod(expectedResultType) .Invoke(null, new[] { executionResult }); } } public 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(CancellationToken cancellationToken = default) { return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator()); } IQueryProvider IQueryable.Provider { get { return new TestAsyncQueryProvider<T>(this); } } } public class TestAsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _inner; public TestAsyncEnumerator(IEnumerator<T> inner) { _inner = inner; } public T Current => _inner.Current; public ValueTask DisposeAsync() { _inner.Dispose(); return ValueTask.CompletedTask; } public ValueTask<bool> MoveNextAsync() { return new ValueTask<bool>(_inner.MoveNext()); } } }
猜测
异步扩展方法中的TransactionScope可能会干扰模拟DbSet的异步查询执行,或者NSubstitute模拟EF Core异步查询结合TransactionScope时存在问题。
内容的提问来源于stack exchange,提问作者Yzak

