测试 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
集成测试的典型步骤包括:
- 使用 Testcontainers 启动一个 Azurite 容器。
- 初始化指向该容器的
BlobServiceClient。 - 填充测试 Blob 及其元数据。
- 调用
BlobSearchService.SearchBlobsAsync并断言结果。
此处省略实现细节,以保持简洁。