如何用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
相关产品推荐
相关产品推荐

