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

.NET Core 9中FastEndpoint端点单元测试报错排查

FastEndpoints端点单元测试的Mock问题与解决

问题背景

在.NET Core 9中使用FastEndpoints实现了GetAllEndpoint端点,集成测试已通过,但编写单元测试时遇到两个核心问题:

  • 无法正确Mock DbSet及其依赖的IQueryable、IAsyncEnumerable异步方法(如ToListAsync)
  • 基类的SendAsync是非虚方法,无法直接Mock,需验证其调用行为

端点代码如下:

public class GetAllEndpoint(AppDbContext db) : EndpointWithoutRequest<List<CategoryDto>>
{
    public override void Configure()
    {
        Get("api/category");
        AllowAnonymous();
        Summary(x =>
        {
            x.Summary = "Get All Categories";
            x.Description = "Fetches a list of all categories.";
        });
    }

    public override async Task HandleAsync(CancellationToken c)
    {
        var categories = await db.Categories
            .Select(category => new CategoryDto(category.Name, category.Description))
            .ToListAsync(c);

        await SendAsync(categories, StatusCodes.Status200OK, c);
    }
}

编写的单元测试运行时抛出错误:

NSubstitute.Exceptions.CouldNotSetReturnDueToNoLastCallException : Could not find a call to return from.

错误原因分析

  1. 异步方法Mock错误:ToListAsync是EF Core的扩展方法,并非DbSet<T>的实例方法,直接Mock_mockCategoriesDbSet.ToListAsync()会导致NSubstitute无法识别该调用,从而抛出异常。需要通过MockIAsyncQueryProvider来支持异步查询。
  2. 端点上下文未初始化:直接调用HandleAsync时,FastEndpoints的内部上下文未设置,SendAsync无法正常执行,同时也无法验证其调用。

修正后的单元测试代码

第一步:实现可Mock的异步查询提供器

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 Task<TResult> ExecuteAsync<TResult>(Expression expression, CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute<TResult>(expression));
    }
}

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

    public T Current => _inner.Current;
}

第二步:修正单元测试类

public class GetAllEndpointTestsNSubstitute
{
    private readonly GetAllEndpoint _endpoint;
    private readonly AppDbContext _mockDbContext;
    private readonly DbSet<Category> _mockCategoriesDbSet;
    private readonly List<Category> _categories;
    // 用于捕获SendAsync的调用参数
    private readonly List<(object Response, int StatusCode)> _sendAsyncCalls = new();

    public GetAllEndpointTestsNSubstitute()
    {
        _categories = new List<Category>
        {
            new Category { Name = "Category1", Description = "Description1" },
            new Category { Name = "Category2", Description = "Description2" }
        };

        // 创建支持异步查询的Queryable
        var categoryQueryable = _categories.AsQueryable();
        var asyncQueryable = new TestAsyncEnumerable<Category>(categoryQueryable);

        // Mock DbSet并实现IQueryable和IAsyncEnumerable接口
        _mockCategoriesDbSet = Substitute.For<DbSet<Category>, IQueryable<Category>, IAsyncEnumerable<Category>>();
        
        // 设置IQueryable成员
        ((IQueryable<Category>)_mockCategoriesDbSet).Provider.Returns(asyncQueryable.Provider);
        ((IQueryable<Category>)_mockCategoriesDbSet).Expression.Returns(categoryQueryable.Expression);
        ((IQueryable<Category>)_mockCategoriesDbSet).ElementType.Returns(categoryQueryable.ElementType);
        ((IQueryable<Category>)_mockCategoriesDbSet).GetEnumerator().Returns(categoryQueryable.GetEnumerator());
        
        // 设置IAsyncEnumerable成员
        ((IAsyncEnumerable<Category>)_mockCategoriesDbSet).GetAsyncEnumerator(Arg.Any<CancellationToken>())
            .Returns(asyncQueryable.GetAsyncEnumerator());

        // Mock DbContext
        _mockDbContext = Substitute.For<AppDbContext>();
        _mockDbContext.Categories.Returns(_mockCategoriesDbSet);

        // 实例化端点并捕获SendAsync调用
        _endpoint = new GetAllEndpoint(_mockDbContext)
        {
            SendAsync = (response, statusCode, ct) =>
            {
                _sendAsyncCalls.Add((response, statusCode));
                return Task.CompletedTask;
            }
        };
    }

    [Fact]
    public async Task HandleAsync_ShouldReturnCategoriesAndCallSendAsync()
    {
        // Arrange
        var cancellationToken = CancellationToken.None;
        var expectedCategories = _categories
            .Select(c => new CategoryDto(c.Name, c.Description))
            .ToList();

        // Act
        await _endpoint.HandleAsync(cancellationToken);

        // Assert
        // 验证SendAsync被调用一次
        Assert.Single(_sendAsyncCalls);
        
        var (returnedResponse, returnedStatusCode) = _sendAsyncCalls[0];
        // 验证返回状态码正确
        Assert.Equal(StatusCodes.Status200OK, returnedStatusCode);
        
        // 验证返回的数据正确
        var returnedCategories = returnedResponse as List<CategoryDto>;
        Assert.NotNull(returnedCategories);
        Assert.Equal(2, returnedCategories.Count);
        Assert.Equal("Category1", returnedCategories[0].Name);
        Assert.Equal("Category2", returnedCategories[1].Name);
    }
}

关键修正说明

  1. 异步查询Mock:通过实现IAsyncQueryProvider和IAsyncEnumerable接口,让Mock的DbSet支持EF Core的异步扩展方法,解决ToListAsync的Mock问题。
  2. SendAsync调用验证:通过在端点实例化时重写SendAsync委托,捕获调用的响应数据和状态码,从而验证端点是否正确执行了返回逻辑。

内容的提问来源于stack exchange,提问作者Hassan Munir

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 15:15:07