如何使用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
相关产品推荐
相关产品推荐

