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

如何用Moq和xUnit对C# API控制器做HTTP请求/响应单元测试?

如何测试API控制器方法的"括号相关内容"(特性、参数与核心逻辑)

看起来你已经搭好了基础测试框架,但还不清楚怎么覆盖控制器方法上的特性(比如权限验证)、参数传递以及Mediator的调用逻辑。下面我会一步步帮你完善这些测试:


1. 测试[BackOfficeRolesAuthorize]权限特性

这个特性用来校验用户是否拥有指定角色,要测试它,我们需要模拟不同角色的用户身份,验证权限控制是否生效:

[Fact]
public async Task ReversePayment_WithAuthorizedRole_ReturnsNoContent()
{
    // Arrange
    var mockMediator = new Mock<IMediator>();
    var controller = new PaymentsController(mockMediator.Object);
    
    // 模拟拥有Admin角色的授权用户
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Role, BackOfficeUserRole.Admin.ToString())
            }))
        }
    };

    // Act
    var result = await controller.ReversePayment(123, 456);

    // Assert
    Assert.IsType<NoContentResult>(result);
    // 验证Mediator确实被调用了
    mockMediator.Verify(m => m.Send(It.IsAny<PaymentStatusChangeManualRequest>(), It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task ReversePayment_WithUnauthorizedRole_ReturnsForbidden()
{
    // Arrange
    var mockMediator = new Mock<IMediator>();
    var controller = new PaymentsController(mockMediator.Object);
    
    // 模拟无权限的角色用户
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Role, "Guest")
            }))
        }
    };

    // Act
    var result = await controller.ReversePayment(123, 456);

    // Assert
    Assert.IsType<ForbidResult>(result);
    // 验证Mediator没有被调用(权限拦截生效)
    mockMediator.Verify(m => m.Send(It.IsAny<PaymentStatusChangeManualRequest>(), It.IsAny<CancellationToken>()), Times.Never);
}

2. 测试核心逻辑:Mediator调用与参数正确性

你的现有测试已经在验证返回值,但更关键的是要确保_mediator.Send被传入了完全正确的参数,这才是业务逻辑的核心:

[Fact]
public async Task ReversePayment_ValidParameters_SendsCorrectMediatorRequest()
{
    // Arrange
    const int testPaymentPlanId = 789;
    const int testPaymentId = 101112;
    var mockMediator = new Mock<IMediator>();
    var controller = new PaymentsController(mockMediator.Object);

    // 先给控制器设置授权用户,避免权限验证干扰核心逻辑测试
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Role, BackOfficeUserRole.CSR.ToString())
            }))
        }
    };

    // Act
    var result = await controller.ReversePayment(testPaymentPlanId, testPaymentId);

    // Assert
    // 验证返回状态码正确
    Assert.IsType<NoContentResult>(result);
    // 验证Mediator被调用时,参数完全匹配我们传入的测试值
    mockMediator.Verify(m => m.Send(
        It.Is<PaymentStatusChangeManualRequest>(req => 
            req.PaymentPlanId == testPaymentPlanId && 
            req.PaymentId == testPaymentId && 
            req.Status == PaymentStatus.Reversed),
        It.IsAny<CancellationToken>()), 
        Times.Once);
}

3. 关于[ProducesResponseType]的测试

这个特性主要用于生成Swagger/OpenAPI文档,单元测试中通常不需要直接测试。如果要验证它的配置是否正确,可以通过ApiExplorer做轻量检查(更偏向集成测试范畴):

[Fact]
public void ReversePayment_ProducesCorrectResponseTypes()
{
    // Arrange
    var methodInfo = typeof(PaymentsController).GetMethod(nameof(PaymentsController.ReversePayment))!;

    // Act
    var producesAttributes = methodInfo.GetCustomAttributes<ProducesResponseTypeAttribute>();

    // Assert
    // 验证NotFound响应的配置
    var notFoundAttr = producesAttributes.FirstOrDefault(a => a.StatusCode == (int)HttpStatusCode.NotFound);
    Assert.NotNull(notFoundAttr);
    Assert.Equal(typeof(ErrorResponse), notFoundAttr.Type);
    
    // 验证NoContent响应的配置
    var noContentAttr = producesAttributes.FirstOrDefault(a => a.StatusCode == (int)HttpStatusCode.NoContent);
    Assert.NotNull(noContentAttr);
}

4. 优化你现有测试的小建议

  • 你的ReversePaymentSetup方法里的断言其实是在测试AutoFixture的配置,这没必要,应该把这些断言移到测试方法里,或者直接验证Mediator的调用参数。
  • 不需要同时用fixture.Freeze<Mock<IMediator>>()和new Mock<IMediator>(),选一种方式即可,推荐用AutoFixture简化依赖注入。
  • 确保每个测试方法独立,不要共享状态。

优化后的AutoFixture版测试示例:

[Fact]
public async Task ReversePayment_WithValidRole_CallsMediatorWithCorrectRequest()
{
    // Arrange
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var mockMediator = fixture.Freeze<Mock<IMediator>>();
    var controller = fixture.Create<PaymentsController>();
    
    // 设置授权用户
    controller.ControllerContext = new ControllerContext
    {
        HttpContext = new DefaultHttpContext
        {
            User = new ClaimsPrincipal(new ClaimsIdentity(new[]
            {
                new Claim(ClaimTypes.Role, BackOfficeUserRole.DealerSupportRep.ToString())
            }))
        }
    };

    var testPaymentPlanId = fixture.Create<int>();
    var testPaymentId = fixture.Create<int>();

    // Act
    var result = await controller.ReversePayment(testPaymentPlanId, testPaymentId);

    // Assert
    Assert.IsType<NoContentResult>(result);
    mockMediator.Verify(m => m.Send(
        It.Is<PaymentStatusChangeManualRequest>(req =>
            req.PaymentPlanId == testPaymentPlanId &&
            req.PaymentId == testPaymentId &&
            req.Status == PaymentStatus.Reversed),
        It.IsAny<CancellationToken>()),
        Times.Once);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:56:27