单元测试中FirstOrDefaultAsync()调用失败的故障排查
FirstOrDefaultAsync()测试问题 我编写了一个根据用户名获取用户ID的异步方法,在编写单元测试时发现:使用FirstOrDefaultAsync()时测试失败,改用同步的FirstOrDefault()则测试正常。
待测试方法
public async Task<int> GetUserId(string userName) { return await _unitofWork.Repository<UserEntity>().Entities .Where(u => u.UserName == userName) .Select(i => i.Id) .FirstOrDefaultAsync(); }
测试代码
[Fact] public async Task GetUserIdTests() { //Arrange var userEntity= new List<UserEntity> { new UserEntity { UserName = "testuser", Id = 1 } }; var repositoryMock = new Mock<IGenericRepository<UserEntity>>(); repositoryMock.Setup(r => r.Entities).Returns(new TestAsyncEnumerable<UserEntity>(userEntity).AsQueryable()); _unitOfWorkMock.Setup(u => u.Repository<UserEntity>()).Returns(repositoryMock.Object); var service= new Service(_userContextMock.Object,_vaultServiceMock.Object); //Act var result = await service.GetUserId("testuser"); //Assert Assert.NotEqual(0, result); }
问题现象
上述测试代码针对FirstOrDefaultAsync()编写,但无法正常运行;改用同步版本FirstOrDefault()时测试完全正常。
支撑代码(异步测试辅助类)
为了在测试中支持异步操作,我实现了TestAsyncEnumerable相关类:
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()); } IAsyncEnumerator<T> IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken) { return GetAsyncEnumerator(cancellationToken); } IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider<T>(this); } public class TestAsyncEnumerator<T> : IAsyncEnumerator<T> { private readonly IEnumerator<T> _inner; public TestAsyncEnumerator(IEnumerator<T> inner) { _inner = inner; } public ValueTask DisposeAsync() { _inner.Dispose(); return ValueTask.CompletedTask; } public ValueTask<bool> MoveNextAsync() { return new ValueTask<bool>(_inner.MoveNext()); } public T Current => _inner.Current; } public class TestAsyncQueryProvider<T> : IAsyncQueryProvider { private readonly IQueryProvider _inner; public TestAsyncQueryProvider(IQueryProvider inner) { _inner = inner; } public IQueryable CreateQuery(Expression expression) { return new TestAsyncEnumerable<T>(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) { return Execute<TResult>(expression); } public IAsyncEnumerable<TResult> ExecuteAsync<TResult>(Expression expression) { return new TestAsyncEnumerable<TResult>(expression); } }
疑问
使用FirstOrDefaultAsync()测试失败的原因是什么?是TestAsyncEnumerable类的实现存在问题,还是异步测试的配置有误?
错误信息
Message: System.ArgumentException : Argument expression is not valid Stack Trace: IQueryProvider.Execute[TElement](Expression expression) TestAsyncQueryProvider`1.Execute[TResult](Expression expression) line 72 TestAsyncQueryProvider`1.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken) line 78 EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, CancellationToken cancellationToken) EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, CancellationToken cancellationToken) EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
问题原因
你的TestAsyncQueryProvider实现存在核心问题:EF Core的FirstOrDefaultAsync调用ExecuteAsync<TResult>时,传入的表达式是针对最终返回类型(此处为int)的异步查询表达式,但你的ExecuteAsync直接复用同步Execute逻辑,而底层EnumerableQuery无法正确解析异步LINQ操作生成的表达式树——它仅支持同步LINQ操作的表达式解析。
修复方案
修改TestAsyncQueryProvider的ExecuteAsync方法,专门处理返回Task<T>类型的异步操作:
public class TestAsyncQueryProvider<T> : IAsyncQueryProvider { private readonly IQueryProvider _inner; public TestAsyncQueryProvider(IQueryProvider inner) { _inner = inner; } // 保留原有其他方法... public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { // 处理返回Task<T>的异步操作(比如FirstOrDefaultAsync返回Task<int>) if (typeof(TResult).IsGenericType && typeof(TResult).GetGenericTypeDefinition() == typeof(Task<>)) { var elementType = typeof(TResult).GetGenericArguments()[0]; // 执行同步查询获取结果 var result = _inner.Execute(expression); // 将结果包装为Task返回 return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult)) .MakeGenericMethod(elementType) .Invoke(null, new[] { result }); } // 其他情况沿用原有逻辑 return Execute<TResult>(expression); } }
或者更简洁的方式,直接将表达式转换为可枚举集合后执行同步逻辑:
public TResult ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken) { if (typeof(TResult).IsGenericType && typeof(TResult).GetGenericTypeDefinition() == typeof(Task<>)) { var elementType = typeof(TResult).GetGenericArguments()[0]; var query = new TestAsyncEnumerable<object>(expression).AsEnumerable(); var result = query.Cast<dynamic>().FirstOrDefault(); return (TResult)typeof(Task).GetMethod(nameof(Task.FromResult)) .MakeGenericMethod(elementType) .Invoke(null, new[] { result }); } return Execute<TResult>(expression); }
另外,也可以直接使用成熟的EF Core测试模拟库(如EntityFrameworkCore.Testing.Moq),避免自己实现IAsyncEnumerable和IAsyncQueryProvider的繁琐与潜在错误。
内容的提问来源于stack exchange,提问作者Jkhan

