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

单元测试中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 19:09:50