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?¶
- Runtime Behavior: Functions are designed to run within the Azure Functions runtime (
func start) - Bindings & Environment: Direct script invocation bypasses bindings, environment setup, and runtime behavior
- Production Parity: HTTP endpoint testing mirrors how functions behave in production
- 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:
✅ 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 startin 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/regionsempty-vnets.json: Empty ARG responsemulti-address-space-vnet.json: VNet with multiple address spacesoverlapping-vnets.json: VNets with overlapping CIDR blocks
Use Load-ArgFixture from TestHelpers.psm1 to load fixtures in tests:
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:
Running Tests¶
Prerequisites¶
Local Development:
- Azurite must be running: Start it with
.\scripts\Start-Azurite.ps1 - Function App will start automatically: Tests use
FunctionTestHarness.psm1to managefunc 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¶
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¶
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.jsonfor 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
BeforeAlland restore them inAfterAll - Use
$Error.Clear()inBeforeEachfor per-test isolation - Use
FunctionTestHarness.psm1to start/stopfunc start - Make HTTP calls to
http://localhost:7071/api/functionname - Use
TestHelpers.psm1for test data setup/teardown - Test the full function behavior through HTTP endpoints
- Clean up test data in
AfterEachorAfterAll - 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
$Requestand$TriggerMetadataparameters - 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¶
- Start function app in
BeforeAll - Seed test data in
BeforeEach - Make HTTP calls using
Invoke-FunctionHttp - Verify responses and Table Storage state
- Clean up in
AfterEach - Stop function app in
AfterAll
Testing Timer Triggers¶
- Start function app in
BeforeAll - Seed test data
- Call admin endpoint using
Invoke-FunctionAdmin - Verify Table Storage state
- 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
funcis installed:func --version - Check port 7071 is available
- Verify
local.settings.jsonis 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.jsonconfiguration
Error Handling and Preferences¶
Recommended Pattern¶
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):
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
-ErrorActionand$ErrorActionPreferenceand can be promoted to terminating errors (Stop) $PSCmdlet.ThrowTerminatingError(): Not affected by-ErrorAction, but is affected by$ErrorActionPreference(and-ErrorAction Breakis a special case the engine checks). This long-standing inconsistency is still discussed in PowerShell GitHub issuesthrowkeyword: 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 changethrow/ThrowTerminatingErrorthemselves$ProgressPreference = 'SilentlyContinue': Suppresses progress overhead, improving CI performanceSuppressAzurePowerShellBreakingChangeWarnings: Environment variable to reduce Azure PowerShell module warning noise$Error.Clear(): Use inBeforeEachfor per-test isolation to prevent error history from affecting subsequent tests