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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 09:25:20