Testing Azure Functions: Unit Tests with Moq & Integration Tests with Testcontainers

Published: (December 7, 2025 at 03:09 PM EST)
4 min read
Source: Dev.to

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

ApproachDescription
Unit Testing with MoqTest function logic in isolation by mocking IBlobSearchService.
Integration Testing with TestcontainersTest 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:

  1. Starting an Azurite container with Testcontainers.
  2. Initialising a BlobServiceClient that points to the container.
  3. Populating test blobs and metadata.
  4. Invoking BlobSearchService.SearchBlobsAsync and asserting the results.

Implementation details are omitted here for brevity.

Back to Blog

Related posts

Read more »