You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

含TransactionScope的EF Core异步查询NSubstitute单元测试报错

问题: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 23:39:56