如何使用NUnit3为C#开发的Azure Function App编写单元测试?
我完全懂你的困惑——Azure Function的Run方法看起来像是个黑盒,不像普通类里的方法好直接Mock测试。其实核心思路是把Function的业务逻辑和触发/绑定逻辑解耦,这样就能单独测试业务部分,再用Mock来模拟Function的上下文和依赖。下面是一套落地的最佳实践:
用NUnit3测试C# Azure Function的完整方案
第一步:抽离业务逻辑到独立服务类
别把所有代码都堆在Run方法里!把核心业务逻辑抽成单独的服务类,让Run只负责接收触发事件、调用业务逻辑、处理输出——这样业务类就能像普通代码一样轻松测试,完全不用管Function的上下文。
举个重构前后的例子:
// 重构前(难测试的Function) public static class OrderFunction { [FunctionName("ProcessOrder")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger log) { // 一堆业务逻辑混在入口里 var order = await req.ReadFromJsonAsync<Order>(); if (order.Total <= 0) return new BadRequestObjectResult("Invalid total"); var inventoryService = new InventoryService(); var isInStock = await inventoryService.CheckStock(order.ProductId); if (!isInStock) return new NotFoundObjectResult("Out of stock"); return new OkObjectResult("Processed"); } }
// 重构后:抽离业务服务(可独立测试) public class OrderProcessingService { private readonly IInventoryService _inventoryService; private readonly ILogger<OrderProcessingService> _logger; // 通过构造函数注入依赖,方便Mock public OrderProcessingService(IInventoryService inventoryService, ILogger<OrderProcessingService> logger) { _inventoryService = inventoryService; _logger = logger; } public async Task<string> ProcessOrderAsync(Order order) { if (order.Total <= 0) { _logger.LogWarning("Invalid order total: {Total}", order.Total); return "Invalid order total"; } var isInStock = await _inventoryService.CheckStock(order.ProductId); if (!isInStock) { _logger.LogInformation("Product {Id} out of stock", order.ProductId); return "Product out of stock"; } return "Order processed"; } } // 简化后的Function(只做管道工作) public class OrderFunction { private readonly OrderProcessingService _processingService; // 构造函数注入业务服务(需要配置Azure Function的DI) public OrderFunction(OrderProcessingService processingService) { _processingService = processingService; } [FunctionName("ProcessOrder")] public async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger log) { var order = await req.ReadFromJsonAsync<Order>(); var result = await _processingService.ProcessOrderAsync(order); return result switch { "Invalid order total" => new BadRequestObjectResult(result), "Product out of stock" => new NotFoundObjectResult(result), _ => new OkObjectResult(result) }; } }
第二步:用NUnit3测试业务服务(核心测试)
这一步和测试普通C#类完全一样,用Moq(或其他Mock框架)模拟依赖,验证业务逻辑的正确性:
using NUnit.Framework; using Moq; using Microsoft.Extensions.Logging; [TestFixture] public class OrderProcessingServiceTests { private Mock<IInventoryService> _mockInventory; private Mock<ILogger<OrderProcessingService>> _mockLogger; private OrderProcessingService _service; [SetUp] public void Setup() { _mockInventory = new Mock<IInventoryService>(); _mockLogger = new Mock<ILogger<OrderProcessingService>>(); _service = new OrderProcessingService(_mockInventory.Object, _mockLogger.Object); } [Test] public async Task ProcessOrderAsync_InvalidTotal_ReturnsError() { // Arrange var invalidOrder = new Order { Total = -50, ProductId = 123 }; // Act var result = await _service.ProcessOrderAsync(invalidOrder); // Assert Assert.That(result, Is.EqualTo("Invalid order total")); // 可选:验证日志是否正确输出 _mockLogger.Verify( x => x.Log( LogLevel.Warning, It.IsAny<EventId>(), It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("-50")), It.IsAny<Exception>(), It.IsAny<Func<It.IsAnyType, Exception, string>>()), Times.Once); } [Test] public async Task ProcessOrderAsync_ProductOutOfStock_ReturnsStockError() { // Arrange var order = new Order { Total = 100, ProductId = 123 }; _mockInventory.Setup(x => x.CheckStock(123)).ReturnsAsync(false); // Act var result = await _service.ProcessOrderAsync(order); // Assert Assert.That(result, Is.EqualTo("Product out of stock")); _mockInventory.Verify(x => x.CheckStock(123), Times.Once); } [Test] public async Task ProcessOrderAsync_ValidOrder_ReturnsSuccess() { // Arrange var order = new Order { Total = 100, ProductId = 123 }; _mockInventory.Setup(x => x.CheckStock(123)).ReturnsAsync(true); // Act var result = await _service.ProcessOrderAsync(order); // Assert Assert.That(result, Is.EqualTo("Order processed")); } }
第三步:测试Function的Run方法(可选,验证输入输出映射)
如果想验证Run方法本身(比如HTTP请求解析、返回结果是否符合预期),可以MockHttpRequest和ILogger:
[TestFixture] public class OrderFunctionTests { private OrderFunction _function; private OrderProcessingService _processingService; [SetUp] public void Setup() { var mockInventory = new Mock<IInventoryService>(); var mockLogger = new Mock<ILogger<OrderProcessingService>>(); _processingService = new OrderProcessingService(mockInventory.Object, mockLogger.Object); _function = new OrderFunction(_processingService); } [Test] public async Task Run_InvalidOrder_ReturnsBadRequest() { // Arrange var invalidOrder = new Order { Total = -50, ProductId = 123 }; var mockReq = CreateMockHttpRequest(invalidOrder); var mockLogger = new Mock<ILogger>(); // Act var result = await _function.Run(mockReq.Object, mockLogger.Object); // Assert Assert.That(result, Is.InstanceOf<BadRequestObjectResult>()); var badRequest = result as BadRequestObjectResult; Assert.That(badRequest.Value, Is.EqualTo("Invalid order total")); } // 辅助方法:创建Mock的HttpRequest private Mock<HttpRequest> CreateMockHttpRequest(object body) { var mockReq = new Mock<HttpRequest>(); var ms = new MemoryStream(); using var writer = new StreamWriter(ms); writer.Write(System.Text.Json.JsonSerializer.Serialize(body)); writer.Flush(); ms.Position = 0; mockReq.Setup(x => x.Body).Returns(ms); return mockReq; } }
关键要点总结
- 解耦是核心:永远不要把业务逻辑和Function的触发/绑定逻辑混在一起,让
Run只做“管道”工作。 - 依赖注入:利用Azure Function的DI能力(.NET 3.1+支持),通过构造函数注入依赖,方便Mock。
- 分层测试:重点测试业务服务单元,
Run方法的测试更多是验证输入输出的映射,不是核心。
内容的提问来源于stack exchange,提问作者Muhammad Shafiq
相关产品推荐
相关产品推荐

