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

使用NUnit和NSubstitute对ASP.NET Core中间件进行单元测试

解决ASP.NET Core中间件单元测试中HttpContext与异常捕获的问题

首先,咱们先理清你当前测试的核心问题:你的测试代码里,传入的next委托并没有抛出异常,所以中间件的try块执行完_next(context)后就直接结束了,根本不会进入catch分支,自然也就不会调用LogException方法。这才是测试不生效的主要原因,而不是HttpContext的模拟问题。

不过先回答你关于HttpContext的疑问:NUnit本身没有专门提供创建真实HttpContext的工具,但ASP.NET Core自带的DefaultHttpContext就是一个真实的、轻量级的HttpContext实现,你在测试里已经用到它了——它不是模拟对象,完全可以满足单元测试的需求。如果后续需要更复杂的上下文(比如配置请求路径、响应状态码等),直接给DefaultHttpContext的属性赋值就行,不需要额外的基础设施。

接下来咱们修正你的测试代码,让它能正确触发中间件的异常处理逻辑:

修正后的测试代码

[TestFixture]
public class ExceptionHelperTests
{
    private IErrorRepository errorRepository;
    private ExceptionHandler handler;

    [SetUp]
    public void Setup()
    {
        errorRepository = Substitute.For<IErrorRepository>();
    }

    // 注意:把async void改成async Task,NUnit对async void的支持很差,会导致测试无法正确等待异步操作完成
    [Test]
    public async Task Given_AnExceptionHappens_Then_ItShouldBeLogged()
    {
        // Arrange
        const string username = "aUser";
        // 创建真实的ClaimsIdentity,而不是完全模拟ClaimsPrincipal,避免Identity.Name返回null
        var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, username) }, "TestAuth");
        var user = new ClaimsPrincipal(identity);

        handler = new ExceptionHandler(
            next: async (innerHttpContext) =>
            {
                innerHttpContext.User = user;
                // 关键:让next委托抛出异常,触发中间件的catch分支
                throw new InvalidOperationException("Test exception");
            },
            repository: errorRepository);

        // Act
        await handler.Invoke(new DefaultHttpContext());

        // Assert
        errorRepository.Received().LogException(
            "An unhandled exception has occurred.", 
            Arg.Any<InvalidOperationException>(), 
            username);
    }
}

关键修改点说明

  1. 将async void改为async Task:NUnit无法正确跟踪async void方法的异步完成状态,会导致测试在异步操作结束前就完成,进而断言失败。
  2. 让next委托抛出异常:这是触发中间件catch分支的核心,没有异常的话,中间件只会执行try块里的逻辑。
  3. 使用真实的ClaimsIdentity:完全模拟ClaimsPrincipal可能会导致Identity.Name返回null(因为模拟对象的默认行为可能不符合预期),用真实的ClaimsIdentity可以更可靠地设置用户名。

关于HttpContext的补充说明

如果你的中间件需要操作响应(比如写入响应内容、设置状态码),DefaultHttpContext的Response属性是真实的HttpResponse实现,完全可以满足需求。你可以直接对它进行配置,比如:

var httpContext = new DefaultHttpContext();
httpContext.Response.Body = new MemoryStream(); // 配置响应流,避免写入时抛出异常

这样修改后,你的测试应该就能正确验证异常是否被记录了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 07:02:34