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

如何使用xUnit和NSubstitute模拟DbSet,解决IAsyncQueryProvider报错

问题原因

Entity Framework Core 提供的异步查询方法(FirstOrDefaultAsync、ToListAsync等)依赖 IAsyncQueryProvider 接口,你直接通过 NSubstitute 模拟的 DbSet<Decoration> 仅实现了 IQueryable 接口,未实现异步查询所需的provider,因此触发报错。

解决方案

步骤1:添加通用异步查询辅助类

先实现一套支持异步查询的通用辅助类,可复用在所有DbSet模拟场景中:

using Microsoft.EntityFrameworkCore.Query;
using System.Linq.Expressions;

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 TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken = default)
    {
        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 });
    }
}

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(CancellationToken cancellationToken = default)
    {
        return new TestAsyncEnumerator<T>(this.AsEnumerable().GetEnumerator());
    }

    IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider<T>(this);
}

internal 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 ValueTask.FromResult(_inner.MoveNext());
    }
}

步骤2:添加List转模拟DbSet的扩展方法

写一个扩展方法快速将普通集合转换为支持异步的模拟DbSet:

using Microsoft.EntityFrameworkCore;
using NSubstitute;

public static class DbSetMockExtensions
{
    public static DbSet<T> ToAsyncDbSetMock<T>(this List<T> sourceList) where T : class
    {
        var asyncEnumerable = new TestAsyncEnumerable<T>(sourceList);
        var mockDbSet = Substitute.For<DbSet<T>, IQueryable<T>, IAsyncEnumerable<T>>();

        ((IAsyncEnumerable<T>)mockDbSet).GetAsyncEnumerator(default).Returns(asyncEnumerable.GetAsyncEnumerator());
        ((IQueryable<T>)mockDbSet).Provider.Returns(asyncEnumerable.Provider);
        ((IQueryable<T>)mockDbSet).Expression.Returns(asyncEnumerable.Expression);
        ((IQueryable<T>)mockDbSet).ElementType.Returns(asyncEnumerable.ElementType);
        ((IEnumerable<T>)mockDbSet).GetEnumerator().Returns(sourceList.GetEnumerator());

        return mockDbSet;
    }
}

步骤3:修改测试代码

替换原来手动模拟DbSet的逻辑,直接用扩展方法生成模拟对象即可:

public class DecorationServiceTests
{
    private readonly DecorationService _subject;
    private readonly IAppDbContext _db = Substitute.For<IAppDbContext>();

    public DecorationServiceTests()
    {
        _subject = new DecorationService(_db);
    }

    [Fact]
    public async Task GetDecorationWithId_ShouldReturnDecoration_WhenExists()
    {
        // Arrange
        var userId = Guid.NewGuid().ToString();
        var decorationId = Guid.NewGuid().ToString();
        var decorations = new List<Decoration>()
        {
            new Decoration()
            {
                Id = decorationId,
                Name = "测试装饰",
                OwnerId = userId,
                // 可按需给BgImage属性赋值,验证Include逻辑
                BgImage = new Image()
            }
        };

        // 直接用扩展方法生成支持异步操作的模拟DbSet
        var mockDecorationDbSet = decorations.ToAsyncDbSetMock();
        _db.Decorations.Returns(mockDecorationDbSet);

        // 无需单独给FirstOrDefaultAsync打桩,模拟DbSet会自动根据集合内容返回数据

        // Act
        var result = await _subject.GetDecorationWithId(userId, decorationId);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(decorationId, result.Id);
    }
}
其他注意事项

如果你的测试场景需要验证Include关联加载逻辑,只需要在构造测试用的Decoration对象时,给对应的导航属性(本例为BgImage)赋值即可,模拟的DbSet会完整保留导航属性数据。

内容的提问来源于stack exchange,提问作者M. Akar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 22:06:07