.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.
错误原因分析
- 异步方法Mock错误:
ToListAsync是EF Core的扩展方法,并非DbSet<T>的实例方法,直接Mock_mockCategoriesDbSet.ToListAsync()会导致NSubstitute无法识别该调用,从而抛出异常。需要通过MockIAsyncQueryProvider来支持异步查询。 - 端点上下文未初始化:直接调用
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); } }
关键修正说明
- 异步查询Mock:通过实现
IAsyncQueryProvider和IAsyncEnumerable接口,让Mock的DbSet支持EF Core的异步扩展方法,解决ToListAsync的Mock问题。 - SendAsync调用验证:通过在端点实例化时重写
SendAsync委托,捕获调用的响应数据和状态码,从而验证端点是否正确执行了返回逻辑。
内容的提问来源于stack exchange,提问作者Hassan Munir
相关产品推荐
相关产品推荐

