测试 Azure Functions:使用 Moq 的单元测试 & 使用 Testcontainers 的集成测试

发布: (2025年12月8日 GMT+8 04:09)
5 min read
原文: Dev.to

Source: Dev.to

概览

使用 Moq 进行单元测试(模拟服务接口)和 Testcontainers 进行集成测试(真实的 Azurite Blob 存储)来测试 Azure Functions。本指南展示了两种方法,使用 BlobMetadataSearch 函数和 IBlobSearchService 模式。

测试方法

方法描述
使用 Moq 的单元测试通过模拟 IBlobSearchService,在隔离环境中测试函数逻辑。
使用 Testcontainers 的集成测试在 Docker 中使用 Azurite(Azure Storage 仿真器)对真实的 BlobSearchService 进行测试。

示例函数:BlobMetadataSearch

一个通过 HTTP 触发的函数,使用依赖注入根据元数据过滤器搜索 Blob。

服务接口

// Interface - Mockable for unit tests
public interface IBlobSearchService
{
    Task> SearchBlobsAsync(
        string containerName,
        string? subFolder,
        List? filters);
}

服务实现

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;

public class BlobSearchService : IBlobSearchService
{
    private readonly BlobServiceClient _blobServiceClient;

    public BlobSearchService(BlobServiceClient blobServiceClient)
    {
        _blobServiceClient = blobServiceClient;
    }

    public async Task> SearchBlobsAsync(
        string containerName,
        string? subFolder,
        List? filters)
    {
        var container = _blobServiceClient.GetBlobContainerClient(containerName);
        var results = new List();
        var prefix = string.IsNullOrWhiteSpace(subFolder) ? null : $"{subFolder.TrimEnd('/')}/";

        await foreach (var blobItem in container.GetBlobsAsync(BlobTraits.Metadata, prefix: prefix))
        {
            if (MatchesFilters(blobItem.Metadata, filters))
            {
                results.Add(new BlobSearchResult
                {
                    FileName = Path.GetFileName(blobItem.Name),
                    FilePath = blobItem.Name,
                    Metadata = blobItem.Metadata.ToDictionary(k => k.Key, v => v.Value)
                });
            }
        }

        return results;
    }

    private bool MatchesFilters(IDictionary metadata, List? filters)
    {
        if (filters == null || filters.Count == 0) return true;

        var normalizedMetadata = metadata.ToDictionary(
            k => k.Key.ToLowerInvariant(),
            v => v.Value);

        bool andMatch = true;
        bool orMatch = false;

        foreach (var filter in filters)
        {
            var key = filter.Key.ToLowerInvariant();
            var matches = normalizedMetadata.TryGetValue(key, out var value) &&
                (filter.FilterType == "contains"
                    ? value.Contains(filter.Value, StringComparison.OrdinalIgnoreCase)
                    : string.Equals(value, filter.Value, StringComparison.OrdinalIgnoreCase));

            if (filter.Condition == "or") orMatch |= matches;
            else andMatch &= matches;
        }

        return andMatch || orMatch;
    }
}

Azure Function 代码 (BlobMetadataSearch.cs)

using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Text.Json;

public class BlobMetadataSearch
{
    private readonly ILogger _logger;
    private readonly IBlobSearchService _searchService;

    public BlobMetadataSearch(
        ILogger logger,
        IBlobSearchService searchService)
    {
        _logger = logger;
        _searchService = searchService;
    }

    [Function("BlobMetadataSearch")]
    public async Task RunAsync(
        [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequestData req)
    {
        var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
        var input = JsonSerializer.Deserialize(
            requestBody,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        if (string.IsNullOrWhiteSpace(input?.Container))
            return new BadRequestObjectResult("Container is required.");

        var results = await _searchService.SearchBlobsAsync(
            input.Container,
            input.SubFolder,
            input.Filters);

        return new OkObjectResult(results);
    }
}

应用启动 (Program.cs)

using Azure.Storage.Blobs;

var host = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(services =>
    {
        services.AddSingleton(sp =>
            new BlobServiceClient(Environment.GetEnvironmentVariable("AzureWebJobsStorage")));

        services.AddTransient();
    })
    .Build();

await host.RunAsync();

单元测试(使用 Moq 的隔离测试)

添加所需的 NuGet 包:

dotnet add package Moq --version 4.20.72
dotnet add package FluentAssertions --version 6.12.2
dotnet add package xunit --version 2.9.2

测试类 (BlobMetadataSearchTests.cs)

using FluentAssertions;
using Moq;
using Xunit;

public class BlobMetadataSearchTests
{
    private readonly Mock _mockSearchService;

    public BlobMetadataSearchTests()
    {
        _mockSearchService = new Mock();
    }

    [Fact]
    public async Task SearchBlobsAsync_WithValidContainer_ReturnsResults()
    {
        // Arrange
        var expectedResults = new List
        {
            new BlobSearchResult
            {
                FileName = "report1.pdf",
                FilePath = "documents/report1.pdf",
                Metadata = new Dictionary { { "department", "finance" } }
            }
        };

        _mockSearchService
            .Setup(s => s.SearchBlobsAsync("test-container", null, null))
            .ReturnsAsync(expectedResults);

        // Act
        var results = await _mockSearchService.Object.SearchBlobsAsync("test-container", null, null);

        // Assert
        results.Should().HaveCount(1);
        results.First().FileName.Should().Be("report1.pdf");
    }

    [Fact]
    public async Task SearchBlobsAsync_WithFilters_PassesFiltersCorrectly()
    {
        // Arrange
        var filters = new List
        {
            new MetadataFilter { Key = "department", Value = "finance", FilterType = "equals", Condition = "and" }
        };

        _mockSearchService
            .Setup(s => s.SearchBlobsAsync("test-container", null, filters))
            .ReturnsAsync(new List());

        // Act
        await _mockSearchService.Object.SearchBlobsAsync("test-container", null, filters);

        // Assert
        _mockSearchService.Verify(s => s.SearchBlobsAsync("test-container", null, filters), Times.Once);
    }

    [Fact]
    public async Task SearchBlobsAsync_WithSubFolder_PassesSubFolderCorrectly()
    {
        // Arrange
        _mockSearchService
            .Setup(s => s.SearchBlobsAsync("test-container", "documents", null))
            .ReturnsAsync(new List());

        // Act
        await _mockSearchService.Object.SearchBlobsAsync("test-container", "documents", null);

        // Assert
        _mockSearchService.Verify(s => s.SearchBlobsAsync("test-container", "documents", null), Times.Once);
    }
}

单元测试的关键优势

  • 快速 – 不需要 Docker 容器。
  • 简单 – 只模拟接口,而不是 Azure SDK。
  • 聚焦 – 只测试函数的 HTTP 处理和验证逻辑。

局限性

  • 测试实际的 Blob 搜索实现。

集成测试(真实的 Azurite 容器)

添加所需的 NuGet 包:

dotnet add package Testcontainers.Azurite --version 4.3.0
dotnet add package xunit --version 2.9.2

集成测试的典型步骤包括:

  1. 使用 Testcontainers 启动一个 Azurite 容器。
  2. 初始化指向该容器的 BlobServiceClient
  3. 填充测试 Blob 及其元数据。
  4. 调用 BlobSearchService.SearchBlobsAsync 并断言结果。

此处省略实现细节,以保持简洁。

Back to Blog

相关文章

阅读更多 »