Skip to content

Testing Guide

This document explains how to test Azure Functions in the IPAM Function App project.

Testing Philosophy

Azure Functions are tested by calling their HTTP endpoints, not by invoking function scripts directly.

This is the idiomatic approach recommended by Microsoft and documented in Azure Functions local development guides.

Why HTTP Endpoint Testing?

  1. Runtime Behavior: Functions are designed to run within the Azure Functions runtime (func start)
  2. Bindings & Environment: Direct script invocation bypasses bindings, environment setup, and runtime behavior
  3. Production Parity: HTTP endpoint testing mirrors how functions behave in production
  4. Microsoft Guidance: This is the pattern documented in Microsoft Learn for Azure Functions testing

What NOT to Do

Don't invoke function scripts directly:

# WRONG - Don't do this
$functionPath = Join-Path $PSScriptRoot '..' '..' 'functions' 'Vendor' 'run.ps1'
$response = & $functionPath -Request $mockRequest -TriggerMetadata $mockTriggerMetadata

Don't create placeholder tests:

# WRONG - Don't do this
$true | Should -Be $false -Because 'Implementation not yet created'

Do use HTTP endpoint testing:

# CORRECT - Use HTTP endpoints
Start-FunctionApp -Port 7071
$response = Invoke-FunctionHttp -Method Post -Route 'getNewNetwork' -Body @{ SessionId = 'test-123' }
$response.StatusCode | Should -Be 200
Stop-FunctionApp

Test Infrastructure

FunctionTestHarness.psm1

The test harness module (Tests/FunctionTestHarness.psm1) provides utilities for testing Azure Functions:

  • Start-FunctionApp: Starts func start in the background
  • Stop-FunctionApp: Stops the running function app
  • Invoke-FunctionHttp: Makes HTTP calls to running functions
  • Invoke-FunctionAdmin: Calls admin endpoints for timer-triggered functions

TestHelpers.psm1

The test helpers module (Tests/TestHelpers.psm1) provides utilities for test data management:

  • Initialize-TestTableStorage: Creates Table Storage client for Azurite
  • Seed-TestPartition: Seeds test data in a partition
  • Clear-TestPartition: Cleans up test data
  • Load-ArgFixture: Loads ARG response fixtures

Test Fixtures

Test fixtures for Azure Resource Graph responses are located in Tests/fixtures/arg/:

  • sample-vnets.json: Sample VNets across multiple subscriptions/regions
  • empty-vnets.json: Empty ARG response
  • multi-address-space-vnet.json: VNet with multiple address spaces
  • overlapping-vnets.json: VNets with overlapping CIDR blocks

Use Load-ArgFixture from TestHelpers.psm1 to load fixtures in tests:

$vnets = Load-ArgFixture -FixtureName 'sample-vnets'

Test Structure

Example: HTTP Trigger Test

BeforeAll {
    # Error and logging setup
    $script:prevErrorAction = $ErrorActionPreference
    $script:prevProgress = $ProgressPreference
    $script:prevWarningEnv = [System.Environment]::GetEnvironmentVariable('SuppressAzurePowerShellBreakingChangeWarnings', 'Process')

    # Reduce noise
    $Error.Clear()
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'SilentlyContinue'
    Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings 'true'

    # Import test infrastructure
    Import-Module (Join-Path $PSScriptRoot '..' 'FunctionTestHarness.psm1')
    Import-Module (Join-Path $PSScriptRoot '..' 'TestHelpers.psm1')

    # Start function app
    Start-FunctionApp -Port 7071

    # Initialize test data
    $script:testClient = Initialize-TestTableStorage
}

AfterAll {
    # Stop function app
    Stop-FunctionApp

    # Restore original state
    $ErrorActionPreference = $script:prevErrorAction
    $ProgressPreference = $script:prevProgress

    if ($null -ne $script:prevWarningEnv)
    {
        Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings $script:prevWarningEnv
    }
    else
    {
        Remove-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings -ErrorAction SilentlyContinue
    }
}

BeforeEach {
    # Clean up test data and clear error history
    $Error.Clear()
    Clear-TestPartition -Client $script:testClient -PartitionKey 'test-partition'
}

It 'Should allocate networks' {
    # Given: Test data is seeded
    Seed-TestPartition -Client $script:testClient -PartitionKey 'test-partition' -Networks @('10.220.4.0/22')

    # When: HTTP endpoint is called
    $body = @{ SessionId = 'test-123' } | ConvertTo-Json
    $response = Invoke-FunctionHttp -Method Post -Route 'getNewNetwork' -Body $body

    # Then: Response is successful
    $response.StatusCode | Should -Be 200
    $responseBody = $response.Content | ConvertFrom-Json
    $responseBody.success | Should -Be $true
}

Example: Timer Trigger Test

For timer-triggered functions, use the admin endpoint:

It 'Should run Manager function' {
    # When: Admin endpoint is called
    $response = Invoke-FunctionAdmin -FunctionName 'Manager'

    # Then: Function executes successfully
    $response.StatusCode | Should -Be 200
}

Test Data Management

Seeding Test Data

Use TestHelpers.psm1 to seed test data:

# Seed a partition with Available networks
Seed-TestPartition `
    -Client $testClient `
    -PartitionKey 'test-partition' `
    -Networks @('10.220.4.0/22', '10.220.8.0/22') `
    -Status 'Available'

Loading Fixtures

Use Load-ArgFixture to load ARG response fixtures:

# Load ARG fixture
$vnets = Load-ArgFixture -FixtureName 'sample-vnets'
$vnets.Count | Should -BeGreaterThan 0

Cleanup

Always clean up test data:

AfterEach {
    Clear-TestPartition -Client $testClient -PartitionKey 'test-partition'
}

Running Tests

Prerequisites

Local Development:

  1. Azurite must be running: Start it with .\scripts\Start-Azurite.ps1
  2. Function App will start automatically: Tests use FunctionTestHarness.psm1 to manage func start

CI Environment:

  • Azurite is automatically started by the CI pipeline
  • Function App is managed by the test harness
  • No manual setup required

Run All Tests

Invoke-Pester

Run Specific Test File

# Run a specific test file
Invoke-Pester -Path Tests\GetNewNetwork.Tests.ps1

# Run with detailed output
Invoke-Pester -Output Detailed -Path Tests\GetNewNetwork.Tests.ps1

Run with Coverage

Invoke-Pester -CodeCoverage modules\*.psm1

Environment Differences

Local Development:

  • Uses Azurite for Table Storage emulation (http://127.0.0.1:10002)
  • Requires manual Azurite startup before running tests
  • Uses local.settings.json for configuration
  • Function App runs on localhost:7071

CI Environment:

  • Azurite is automatically started by the test workflow
  • Uses environment variables for configuration
  • Same test execution pattern, but automated setup

Writing Tests

Best Practices

DO:

  • Configure error handling and logging preferences in BeforeAll and restore them in AfterAll
  • Use $Error.Clear() in BeforeEach for per-test isolation
  • Use FunctionTestHarness.psm1 to start/stop func start
  • Make HTTP calls to http://localhost:7071/api/functionname
  • Use TestHelpers.psm1 for test data setup/teardown
  • Test the full function behavior through HTTP endpoints
  • Clean up test data in AfterEach or AfterAll
  • Use descriptive test names that explain what is being tested

DON'T:

  • Invoke function scripts directly (e.g., & functions/Vendor/run.ps1)
  • Try to mock $Request and $TriggerMetadata parameters
  • Bypass the Azure Functions runtime
  • Create placeholder tests with $true | Should -Be $false
  • Leave test data in Table Storage after tests complete

Common Patterns

Testing HTTP Triggers

  1. Start function app in BeforeAll
  2. Seed test data in BeforeEach
  3. Make HTTP calls using Invoke-FunctionHttp
  4. Verify responses and Table Storage state
  5. Clean up in AfterEach
  6. Stop function app in AfterAll

Testing Timer Triggers

  1. Start function app in BeforeAll
  2. Seed test data
  3. Call admin endpoint using Invoke-FunctionAdmin
  4. Verify Table Storage state
  5. Stop function app in AfterAll

Testing Error Cases

Test error responses by:

  • Seeding invalid test data
  • Making invalid HTTP requests
  • Verifying error status codes and messages

Troubleshooting

Function App Not Starting

  • Ensure func is installed: func --version
  • Check port 7071 is available
  • Verify local.settings.json is valid

Tests Hanging

  • Check if function app is already running
  • Verify Azurite is running
  • Check for port conflicts

HTTP Call Failures

  • Verify function app is running: Get-Process func -ErrorAction SilentlyContinue
  • Check function app logs in the terminal
  • Verify route matches function.json configuration

Error Handling and Preferences

Tests should configure error handling and logging preferences at the start of BeforeAll and restore them in AfterAll. This pattern ensures consistent test behavior and prevents non-terminating errors from interfering with HTTP status code assertions.

In BeforeAll:

BeforeAll {
    # Error and logging setup
    $script:prevErrorAction = $ErrorActionPreference
    $script:prevProgress = $ProgressPreference
    $script:prevWarningEnv = [System.Environment]::GetEnvironmentVariable('SuppressAzurePowerShellBreakingChangeWarnings', 'Process')

    # Reduce noise
    $Error.Clear()
    $ErrorActionPreference = 'Stop'
    $ProgressPreference = 'SilentlyContinue'
    Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings 'true'

    # ... rest of setup ...
}

In AfterAll:

AfterAll {
    # ... cleanup ...

    # Restore original state
    $ErrorActionPreference = $script:prevErrorAction
    $ProgressPreference = $script:prevProgress

    if ($null -ne $script:prevWarningEnv)
    {
        Set-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings $script:prevWarningEnv
    }
    else
    {
        Remove-Item Env:\SuppressAzurePowerShellBreakingChangeWarnings -ErrorAction SilentlyContinue
    }
}

In BeforeEach (if using per-test isolation):

BeforeEach {
    $Error.Clear()
    # ... test setup ...
}

Why This Pattern?

Tests assert on HTTP status codes rather than catching errors thrown by Write-Error. The test harness (FunctionTestHarness.psm1) catches Invoke-WebRequest -ErrorAction Stop exceptions that have an HttpWebResponse, extracts the status code and body, and returns a result object. Tests then assert on this result object.

Transport and precondition failures (no HTTP response) bubble to the outer catch block, where $PSCmdlet.ThrowTerminatingError($_) appropriately halts the pipeline for infrastructure failures.

PowerShell Error Handling Behavior

Understanding PowerShell's error handling is critical for writing reliable tests:

Statement-Terminating vs. Script-Terminating Errors

PowerShell distinguishes between:

  • Statement-terminating errors: The current pipeline/statement stops, but the script may continue
  • Script/runspace-terminating errors: Fatal errors that stop execution of the entire script/runspace

$PSCmdlet.ThrowTerminatingError() Behavior

In an advanced cmdlet/function, calling $PSCmdlet.ThrowTerminatingError(<ErrorRecord>) terminates the command and causes PowerShell to raise PipelineStoppedException, which halts the pipeline (Begin/Process/End) and prevents further output/writes in that pipeline.

The API documentation explicitly notes: "ThrowTerminatingError terminates the command, where WriteError allows the command to continue" and it always results in PipelineStoppedException.

Practically, inside Begin/Process/End blocks, this is the "cmdlet/pipeline-terminating" signal. The host or next statement may still continue unless escalated to script/runspace termination, which is where preferences enter.

Interaction with -ErrorAction vs. $ErrorActionPreference

  • Cmdlet-emitted non-terminating errors: Respect -ErrorAction and $ErrorActionPreference and can be promoted to terminating errors (Stop)
  • $PSCmdlet.ThrowTerminatingError(): Not affected by -ErrorAction, but is affected by $ErrorActionPreference (and -ErrorAction Break is a special case the engine checks). This long-standing inconsistency is still discussed in PowerShell GitHub issues
  • throw keyword: Generates a terminating error; typically script-terminating unless caught, and does not depend on -ErrorAction (it's not a cmdlet error)

The developer documentation "Terminating Errors" confirms that once a cmdlet reports a terminating error, the pipeline is permanently stopped and subsequent writes throw PipelineStoppedException.

Setting Preferences in Tests

  • $ErrorActionPreference = 'Stop': Promotes non-terminating errors to terminating, giving deterministic behavior. Does not change throw/ThrowTerminatingError themselves
  • $ProgressPreference = 'SilentlyContinue': Suppresses progress overhead, improving CI performance
  • SuppressAzurePowerShellBreakingChangeWarnings: Environment variable to reduce Azure PowerShell module warning noise
  • $Error.Clear(): Use in BeforeEach for per-test isolation to prevent error history from affecting subsequent tests

References