Testing Azure Functions: Unit Tests with Moq & Integration Tests with Testcontainers
Source: Dev.to
Overview
Test Azure Functions using Moq for unit tests (mocked service interfaces) and Testcontainers for integration tests (real Azurite blob storage). This guide shows both approaches using the BlobMetadataSearch function with the IBlobSearchService pattern.
Testing Approaches
| Approach | Description |
|---|---|
| Unit Testing with Moq | Test function logic in isolation by mocking IBlobSearchService. |
| Integration Testing with Testcontainers | Test the real BlobSearchService against Azurite (Azure Storage emulator) in Docker. |
Example Function: BlobMetadataSearch
An HTTP‑triggered function that searches blobs by metadata filters using dependency injection.
Service Interface
// Interface - Mockable for unit tests
public interface IBlobSearchService
{
Task> SearchBlobsAsync(
string containerName,
string? subFolder,
List? filters);
}
Service Implementation
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 Code (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);
}
}
Application Startup (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();
Unit Tests (Isolation with Moq)
Add the required packages:
dotnet add package Moq --version 4.20.72
dotnet add package FluentAssertions --version 6.12.2
dotnet add package xunit --version 2.9.2
Test Class (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);
}
}
Key Benefits of Unit Testing
- Fast – No Docker containers required.
- Simple – Mock the interface, not the Azure SDK.
- Focused – Tests only the function’s HTTP handling and validation logic.
Limitations
- Does not test the actual blob search implementation.
Integration Tests (Real Azurite Container)
Add the required packages:
dotnet add package Testcontainers.Azurite --version 4.3.0
dotnet add package xunit --version 2.9.2
The integration test setup typically involves:
- Starting an Azurite container with Testcontainers.
- Initialising a
BlobServiceClientthat points to the container. - Populating test blobs and metadata.
- Invoking
BlobSearchService.SearchBlobsAsyncand asserting the results.
Implementation details are omitted here for brevity.