Recording Framework for Azure Developer CLI Functional Tests

July 20, 2026 · View on GitHub

Table of Contents


Overview

The Azure Developer CLI (azd) uses a sophisticated recording framework for functional tests that allows tests to:

  • Run against live Azure resources and record HTTP interactions
  • Replay recorded interactions for fast, deterministic testing
  • Support both HTTP/HTTPS traffic and command-line tool invocations (like docker, dotnet)

The recording framework is based on go-vcr but extends it with:

  • HTTP proxy server support for recording/playback
  • Command proxy support for tools like Docker and dotnet
  • Variable storage across test runs
  • Automatic sanitization of sensitive data
  • Smart matching for dynamic Azure resources

How Recording Works

Architecture

The recording system consists of several components:

┌─────────────────┐
│  Functional     │
│  Test Code      │
└────────┬────────┘

         ├──► recording.Start(t)


┌─────────────────────────────────────┐
│  Recording Session                  │
│  - ProxyUrl (HTTPS proxy)          │
│  - CmdProxyPaths (for docker, etc) │
│  - Variables (env names, etc)      │
│  - ProxyClient (HTTP client)       │
└─────────────────────────────────────┘

         ├──► HTTP Traffic ──► go-vcr recorder ──► YAML cassettes

         └──► Command Calls ──► cmdrecord proxies ──► YAML cassettes

Recording Modes

The framework supports four modes (controlled by AZURE_RECORD_MODE env var):

  1. live - No recording/playback, direct passthrough to Azure
  2. record - Records all interactions, overwrites existing recordings
  3. playback - Only replays from recordings, fails if not found
  4. recordOnce (default for local dev) - Records if no cassette exists, otherwise plays back

In CI environments, the default is determined by whether recordings exist.

What Gets Recorded

HTTP/HTTPS Traffic

  • All Azure Resource Manager API calls
  • Azure Storage operations
  • Container Registry interactions
  • Most Azure service APIs

Command Invocations

The framework can record specific command invocations:

  • Docker: docker login, docker push
  • Dotnet: dotnet publish with ContainerRegistry parameter

Variables Stored

Session variables are stored separately from interactions:

  • env_name - Azure environment name
  • subscription_id - Azure subscription ID
  • time - Unix timestamp of recording

Recording Capabilities

HTTP Recording Features

Automatic Sanitization

The framework automatically sanitizes sensitive data:

  • Authorization headers → SANITIZED
  • Container registry tokens
  • Storage account SAS signatures
  • Key Vault secrets
  • Container app secrets

Smart Matching

Custom matchers handle dynamic Azure resources:

  • Role assignment GUIDs are ignored in matching
  • Container app operation result query parameters are ignored
  • Host mapping for httptest.NewServer URLs

Passthrough for Personal Data

Certain endpoints bypass recording to avoid storing personal information:

  • login.microsoftonline.com
  • graph.microsoft.com
  • applicationinsights.azure.com
  • azd release/update endpoints

Fast-Forward Polling

Long-running operations are automatically fast-forwarded during recording to avoid storing hundreds of polling requests.

Command Recording Features

The cmdrecord package intercepts specific command invocations:

cmdrecord.NewWithOptions(cmdrecord.Options{
    CmdName:      "docker",
    CassetteName: name,
    RecordMode:   opt.mode,
    Intercepts: []cmdrecord.Intercept{
        {ArgsMatch: "^login"},
        {ArgsMatch: "^push"},
    },
})

This creates a proxy executable that:

  1. Intercepts matching command invocations
  2. Records inputs/outputs to a separate YAML file
  3. Replays during playback mode

Creating a New Functional Test with Recording

Step-by-Step Guide

1. Create Your Test Function

func Test_CLI_MyNewFeature(t *testing.T) {
    t.Parallel() // Most tests run in parallel

    ctx, cancel := newTestContext(t)
    defer cancel()

    // Create a temporary directory for test files
    dir := tempDirWithDiagnostics(t)
    t.Logf("DIR: %s", dir)

    // Start the recording session
    session := recording.Start(t)
    
    // Generate or retrieve environment name
    envName := randomOrStoredEnvName(session)
    t.Logf("AZURE_ENV_NAME: %s", envName)

    // Create CLI with recording session
    cli := azdcli.NewCLI(t, azdcli.WithSession(session))
    cli.WorkingDirectory = dir
    cli.Env = append(cli.Env, os.Environ()...)
    cli.Env = append(cli.Env, "AZURE_LOCATION=eastus2")

    // Setup cleanup (only runs in live mode, not playback)
    defer cleanupDeployments(ctx, t, cli, session, envName)

    // ... rest of test logic
}

2. Use Session-Aware HTTP Clients

For Azure SDK clients, use the session's ProxyClient:

var client *http.Client
subscriptionId := cfg.SubscriptionID

if session != nil {
    client = session.ProxyClient
    
    if session.Playback {
        // Use recorded subscription ID
        subscriptionId = session.Variables[recording.SubscriptionIdKey]
    }
} else {
    client = http.DefaultClient
}

// Create Azure SDK client with session transport
cred := azdcli.NewTestCredential(cli)
rgClient, err := armresources.NewResourceGroupsClient(subscriptionId, cred, &arm.ClientOptions{
    ClientOptions: azcore.ClientOptions{
        Transport: client,
    },
})

3. Store and Retrieve Variables

For values that change between recording and playback:

// During test execution
if session != nil {
    // This will be stored in the recording
    session.Variables[recording.SubscriptionIdKey] = env[environment.SubscriptionIdEnvVarName]
}

// When reading back
if session != nil && session.Playback {
    subscriptionId = session.Variables[recording.SubscriptionIdKey]
}

Use randomOrStoredEnvName() helper for environment names:

// Automatically handles recording vs playback
envName := randomOrStoredEnvName(session)

4. Handle Time-Dependent Operations

For operations that validate timing or poll:

if session == nil {
    // Live mode - use real delays
    err = probeServiceHealth(
        t, ctx, http.DefaultClient, 
        retry.NewConstant(5*time.Second), 
        url, expectedResponse)
} else {
    // Recording/playback mode - use minimal delays
    err = probeServiceHealth(
        t, ctx, session.ProxyClient, 
        retry.NewConstant(1*time.Millisecond), 
        url, expectedResponse)
}

5. Add Cleanup Logic

defer cleanupDeployments(ctx, t, cli, session, envName)

This helper (from test/functional/aspire_test.go) deletes subscription deployments only in live mode.


Re-recording an Existing Test

Note: Re-recording is typically a core-maintainer workflow. External contributors generally do not need to re-record cassettes; the default playback mode replays stored interactions and requires no Azure access. Open an issue if a cassette appears stale.

The easiest way to re-record tests is the mage record target:

cd cli/azd
mage record -filter=Test_CLI_MyNewFeature   # re-record a specific test
mage record                                  # re-record all playback tests

mage record handles building the azd-record binary, setting AZURE_RECORD_MODE=record, and running the test with a 30-minute timeout.

Configuring the Test Subscription

Tests need an Azure subscription and tenant. Any subscription you have access to works — the defaults.test.* keys are a separate namespace so test configuration does not affect regular azd defaults. Configure them once with azd config (persists across sessions):

azd config set defaults.test.subscription <SUBSCRIPTION_ID>
azd config set defaults.test.tenant <TENANT_ID>

The resolution order is: environment variable → defaults.test.* config → defaults.* config. Note: tenant only falls back to defaults.test.tenant (no defaults.tenant global fallback). Config fallbacks are only consulted when the CI environment variable is unset.

SettingEnvironment VariableConfig Key
SubscriptionAZD_TEST_AZURE_SUBSCRIPTION_IDdefaults.test.subscription
TenantAZD_TEST_TENANT_IDdefaults.test.tenant
LocationAZD_TEST_AZURE_LOCATIONdefaults.test.location

Manual Re-recording

If you prefer manual control:

  1. Delete existing recording:

    rm test/functional/testdata/recordings/Test_CLI_MyNewFeature.yaml
    rm test/functional/testdata/recordings/Test_CLI_MyNewFeature.*.yaml  # if command recordings exist
    
  2. Set recording mode:

    export AZURE_RECORD_MODE=record
    
  3. Ensure you're authenticated:

    azd auth login
    
  4. Run the test:

    cd cli/azd
    go test -v -run ^Test_CLI_MyNewFeature$ ./test/functional -timeout 30m
    
  5. Verify recordings were created:

    ls -lh test/functional/testdata/recordings/Test_CLI_MyNewFeature*
    

What Happens During Recording

  1. azd binary Built with Record Tag: The test automatically builds azd-record with the record build tag
  2. Recording Proxy Starts: An HTTPS proxy starts at a random port
  3. Command Proxies Start: Proxy executables for docker, dotnet are placed in temporary directories
  4. Environment Variables Set:
    • AZD_TEST_HTTPS_PROXY → points to recording proxy
    • PATH → prepended with command proxy paths
    • AZD_DEBUG_PROVISION_PROGRESS_DISABLE=true
  5. Test Executes: All HTTP calls and command invocations go through proxies
  6. Cassettes Saved: On test success, interactions are saved to YAML files

Recording File Structure

After recording, you'll see files like:

test/functional/testdata/recordings/
├── Test_CLI_MyNewFeature.yaml          # HTTP interactions
├── Test_CLI_MyNewFeature.docker.yaml   # Docker command recordings (if used)
└── Test_CLI_MyNewFeature.dotnet.yaml   # Dotnet command recordings (if used)

Each YAML file contains:

---
version: 2
interactions:
  - id: 0
    request:
      method: PUT
      url: https://management.azure.com/...
      headers:
        Authorization: SANITIZED
      body: '...'
    response:
      status: 200 OK
      headers: {...}
      body: '...'
  - id: 1
    # ... more interactions
---
env_name: azdtest-w4c1619
subscription_id: faa080af-c1d8-40ad-9cce-e1a450ca5b57
time: "1744738873"

Building azd with Recording Support

Build Tags

azd has two build configurations relevant to testing:

  1. Standard build (no tags):

    go build -o azd
    
    • No recording support
    • Uses deps.go (standard HTTP client, real clock)
  2. Recording build (-tags=record):

    go build -tags=record -o azd-record
    
    • Includes recording support
    • Uses deps_record.go (accepts recording proxy settings, can use fixed clock)
    • Required for recording new tests or re-recording

What the record Build Tag Enables

When built with -tags=record, azd:

  1. Accepts AZD_TEST_HTTPS_PROXY environment variable:

    // cmd/deps_record.go
    if val, ok := os.LookupEnv("AZD_TEST_HTTPS_PROXY"); ok {
        proxyUrl, err := url.Parse(val)
        transport.Proxy = http.ProxyURL(proxyUrl)
    }
    
  2. Uses self-signed certificates:

    transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
    
  3. Supports fixed clock (via AZD_TEST_FIXED_CLOCK_UNIX_TIME):

    func createClock() clock.Clock {
        if fixed, ok := fixedClock(); ok {
            return fixed
        }
        return clock.New()
    }
    

Automatic Build Behavior

The test framework automatically builds the correct binary:

// test/azdcli/cli.go
if opt.Session != nil {
    buildRecordOnce.Do(func() {
        build(t, sourceDir, "-tags=record", "-o=azd-record")
    })
} else {
    buildOnce.Do(func() {
        build(t, sourceDir)
    })
}

Skip automatic build:

export CLI_TEST_SKIP_BUILD=true

Use custom binary path:

export CLI_TEST_AZD_PATH=/path/to/my/azd-record

Using Recordings in CI

CI Configuration

In CI environments (detected via CI environment variable), the framework:

  1. Uses ModeRecordOnce by default (if AZURE_RECORD_MODE not set)
  2. Expects recordings to exist
  3. Fails with helpful message if missing:
    failed to load recordings: file not found: 
    to record this test, re-run the test with AZURE_RECORD_MODE='record'
    

CI Build Process

The ci-build.ps1 script has a -BuildRecordMode flag:

# ci-build.ps1
param(
    [switch] $BuildRecordMode,
    # ...
)

if ($BuildRecordMode) {
    $buildFlags += "-tags=record"
    Write-Host "Building with record tag enabled"
    & go build $buildFlags -o $outputPath
} else {
    Write-Host "Building standard binary"
    & go build -o azd
}

Build both binaries in CI:

# Standard build
./ci-build.ps1

# Recording build (for tests)
./ci-build.ps1 -BuildRecordMode

CI Test Execution

# ci-test.ps1 runs tests in two phases:

# 1. Unit tests (short mode)
gotestsum -- ./... -short -v -cover

# 2. Integration/Functional tests
gotestsum -- ./... -v -timeout 120m

The functional tests automatically:

  • Use recordings if available (fast, no Azure calls)
  • Skip automatic build (binaries pre-built)
  • Use CLI_TEST_AZD_PATH if set
# Pseudo CI configuration
steps:
  - name: Build standard azd
    run: ./ci-build.ps1
    
  - name: Build azd-record for tests
    run: ./ci-build.ps1 -BuildRecordMode
    
  - name: Run tests with recordings
    env:
      AZURE_RECORD_MODE: playback  # Force playback mode
      CLI_TEST_AZD_PATH: ./azd-record
    run: ./ci-test.ps1

Recording for Extensions

Extension Testing with Recording

Extensions present unique challenges for recording because:

  1. Extensions make gRPC callbacks to the main azd process
  2. Extension operations often trigger Azure API calls through azd
  3. Extension installation/build happens outside the recording session

Example: Test_CLI_Extension_Capabilities

func Test_CLI_Extension_Capabilities(t *testing.T) {
    // Skip in playback mode - extensions are too complex to record
    session := recording.Start(t)
    if session != nil && session.Playback {
        t.Skip("Skipping test in playback mode. This test is live only.")
    }

    // Generate env name before session for use in both phases
    envName := randomOrStoredEnvName(session)

    // Phase 1: Extension setup (NOT recorded)
    cliNoSession := azdcli.NewCLI(t)  // No session
    cliNoSession.WorkingDirectory = dir
    
    _, err := cliNoSession.RunCommand(ctx, "ext", "install", "microsoft.azd.extensions")
    require.NoError(t, err)
    
    // Phase 2: Main test (CAN be recorded)
    cli := azdcli.NewCLI(t, azdcli.WithSession(session))
    cli.WorkingDirectory = dir
    
    _, err = cli.RunCommandWithStdIn(ctx, stdinForInit(envName), "init")
    require.NoError(t, err)
}

Why Extensions Are Challenging

  1. gRPC Callbacks: Extensions communicate with azd via gRPC, which isn't captured by HTTP recording
  2. Nested Operations: Extension calls trigger azd commands, creating complex recording chains
  3. Build Dependencies: Extensions need to be built before tests, outside recording context

Strategies for Extension Testing

Option 1: Live-Only Tests (Current Approach)

if session != nil && session.Playback {
    t.Skip("Skipping test in playback mode. This test is live only.")
}

Pros: Simple, reliable Cons: Slow in CI, requires Azure credentials

Option 2: Partial Recording

// Install extension without recording
cliNoSession := azdcli.NewCLI(t)
_, err := cliNoSession.RunCommand(ctx, "ext", "install", "...")

// Record the actual test operations
cli := azdcli.NewCLI(t, azdcli.WithSession(session))
_, err = cli.RunCommand(ctx, "up")

Pros: Records Azure interactions Cons: Still requires extension installation in CI

Option 3: Mock Extension Services

For unit tests of extension framework:

// Use mock gRPC services
mockExtension := &mockExtensionService{...}
// Test framework behavior without real extensions

Pros: Fast, deterministic Cons: Doesn't test real extension integration

Extension CI Builds

Extensions also use recording build tags:

# extensions/microsoft.azd.demo/ci-build.ps1
if ($BuildRecordMode) {
    $buildFlags += "-tags=record"
}
& go build $buildFlags -o azd-ext-microsoft-azd-demo

This allows extensions themselves to be tested with recordings if needed.


Best Practices and Tips

1. Use randomOrStoredEnvName() Helper

DO:

envName := randomOrStoredEnvName(session)

DON'T:

envName := randomEnvName()  // Same name won't be used in playback!

2. Always Check Session Mode for Live Operations

DO:

if session != nil && session.Playback {
    // Skip cleanup in playback
    return
}
// Perform cleanup
client.DeleteResourceGroup(...)

DON'T:

// Always try cleanup
client.DeleteResourceGroup(...)  // Will fail in playback mode!

3. Use Session's ProxyClient for HTTP Operations

DO:

client := http.DefaultClient
if session != nil {
    client = session.ProxyClient
}
azureClient, err := armresources.NewResourceGroupsClient(subId, cred, &arm.ClientOptions{
    ClientOptions: azcore.ClientOptions{
        Transport: client,
    },
})

4. Minimize Live Test Dependencies

DO:

// Copy sample project to temp dir
err := copySample(dir, "webapp")

// Run azd commands
_, err = cli.RunCommand(ctx, "init")

DON'T:

// Clone from GitHub (not recorded!)
exec.Command("git", "clone", "https://github.com/...").Run()

5. Store Dynamic Values in Session Variables

DO:

if session != nil {
    session.Variables[recording.SubscriptionIdKey] = actualSubId
    session.Variables["custom_resource_id"] = resourceId
}

6. Use Appropriate Timeouts

DO:

if session == nil {
    // Live - real delays
    time.Sleep(5 * time.Second)
} else {
    // Playback - minimal delays
    time.Sleep(1 * time.Millisecond)
}

7. Isolate State and Use Shared Cleanup

Use a private AZD_CONFIG_DIR when a test changes user-level state so parallel tests do not share ~/.azd. Set it on the test CLI before the first azd invocation rather than using t.Setenv, which changes process-wide state:

configDir := tempDirWithDiagnostics(t)
cli.Env = append(os.Environ(), cli.Env...)
cli.Env = append(cli.Env,
    "AZD_CONFIG_DIR="+configDir,
    "AZURE_DEV_COLLECT_TELEMETRY=no",
)

A private config dir is not the default for functional tests because live tests inherit the pipeline's one-time azd auth login from the shared config dir: a new config directory is logged out, and there is currently no supported way to authenticate one in CI - leave AZD_CONFIG_DIR unset when a test needs live Azure access. Playback tests are unaffected: recording sessions authenticate through the session's credential server, not the config dir.

Use tempDirWithDiagnostics(t) for generated or executed files. It retries transient Windows file locks and reports lock diagnostics if cleanup fails. Register other cleanup when state is created; don't duplicate retry loops or add fixed sleeps. Since t.Context() is canceled before cleanup runs, use a separate bounded context when needed.

Guard type assertions when reading untyped fixture, JSON, environment, or recording data so malformed input fails the test instead of panicking:

subscriptionID, ok := envValues["AZURE_SUBSCRIPTION_ID"].(string)
require.True(t, ok, "AZURE_SUBSCRIPTION_ID should be a string")

8. Add Debug Logging

t.Logf("Recording mode: playback=%v", session != nil && session.Playback)
t.Logf("Environment name: %s", envName)
t.Logf("Subscription ID: %s", subscriptionId)

9. Handle Skipped Tests Gracefully

DO:

func Test_CLI_ComplexFeature(t *testing.T) {
    session := recording.Start(t)
    
    // Skip if specific conditions aren't met
    if session != nil && session.Playback && someComplexCondition {
        t.Skip("This test requires live mode for complex operations")
    }
    
    // ... test continues
}

Troubleshooting

Problem: Recording Not Found

Error:

failed to load recordings: file not found
to record this test, re-run the test with AZURE_RECORD_MODE='record'

Solutions:

  1. Set AZURE_RECORD_MODE=record
  2. Run test to create recording
  3. Commit the recording file to git

Problem: Test Fails During Playback But Passes Live

Symptoms:

  • Test passes with AZURE_RECORD_MODE=live
  • Test fails with AZURE_RECORD_MODE=playback

Common Causes:

  1. Using wrong HTTP client:

    // WRONG - bypasses proxy
    client := http.DefaultClient
    
    // RIGHT - uses session client
    client := http.DefaultClient
    if session != nil {
        client = session.ProxyClient
    }
    
  2. Hard-coded values instead of session variables:

    // WRONG - uses live subscription
    subId := cfg.SubscriptionID
    
    // RIGHT - uses recorded subscription
    subId := cfg.SubscriptionID
    if session != nil && session.Playback {
        subId = session.Variables[recording.SubscriptionIdKey]
    }
    
  3. Not storing subscription ID in session:

    // Add this after provision/deployment:
    if session != nil {
        session.Variables[recording.SubscriptionIdKey] = env.GetSubscriptionId()
    }
    

Problem: Request Not Matching Recorded Interaction

Error:

could not find matching request

Diagnosis:

  1. Enable debug logging: export RECORDER_PROXY_DEBUG=1
  2. Run test and check logs for matching differences

Common Causes:

  1. Query parameter differences:

    • Check if query params are in different order
    • Consider adding custom matcher
  2. Dynamic request IDs or timestamps:

    • These should be sanitized
    • Check sanitization code in test/recording/sanitize.go
  3. Host differences:

    • Use WithHostMapping option if needed:
    session := recording.Start(t, recording.WithHostMapping(server.URL, "localhost:8080"))
    

Problem: Sensitive Data in Recordings

Issue: Authorization tokens, SAS signatures, or secrets appearing in recordings

Solutions:

  1. Check existing sanitization: Look at test/recording/sanitize.go for examples

  2. Add custom sanitization:

    // In recording.go, add a new hook:
    vcr.AddHook(func(i *cassette.Interaction) error {
        if strings.Contains(i.Request.URL, "/myservice/") {
            i.Request.Headers.Set("X-Custom-Token", "SANITIZED")
        }
        return nil
    }, recorder.BeforeSaveHook)
    
  3. Add passthrough for sensitive endpoints:

    vcr.AddPassthrough(func(req *http.Request) bool {
        return strings.Contains(req.URL.Host, "sensitive-service.com")
    })
    

Problem: Command Not Being Recorded

Symptoms: Docker or dotnet commands execute but aren't recorded

Diagnosis:

  1. Check if command proxy is in PATH:

    t.Logf("PATH: %s", os.Getenv("PATH"))
    
  2. Check if command matches intercept pattern:

    // Only these patterns are intercepted for docker:
    Intercepts: []cmdrecord.Intercept{
        {ArgsMatch: "^login"},
        {ArgsMatch: "^push"},
    }
    

Solutions:

  1. Add new intercept pattern:

    // In recording.go
    recorders = append(recorders, cmdrecord.NewWithOptions(cmdrecord.Options{
        CmdName:      "docker",
        CassetteName: name,
        RecordMode:   opt.mode,
        Intercepts: []cmdrecord.Intercept{
            {ArgsMatch: "^login"},
            {ArgsMatch: "^push"},
            {ArgsMatch: "^build"},  // Add new pattern
        },
    }))
    
  2. Verify proxy is first in PATH:

    cli.Env = append(cli.Env, 
        "PATH="+strings.Join(session.CmdProxyPaths, string(os.PathListSeparator))+
        string(os.PathListSeparator)+os.Getenv("PATH"))
    

Problem: Recordings Are Too Large

Issue: Recording files over 1MB, causing git/review issues

Solutions:

  1. Enable response trimming (already enabled for deployments):

    • Large deployment responses are automatically trimmed
    • See test/recording/trim_response.go
  2. Add polling fast-forward:

    • Polling operations are automatically discarded
    • See httpPollDiscarder in recording.go
  3. Split test into smaller tests:

    // Instead of one large Test_CLI_FullWorkflow
    func Test_CLI_Provision(t *testing.T) { ... }
    func Test_CLI_Deploy(t *testing.T) { ... }
    func Test_CLI_Down(t *testing.T) { ... }
    

Problem: Test Timing Out

Symptoms: Test runs forever in recording mode

Common Causes:

  1. Waiting for resources that never complete:

    • Check if provision is hanging
    • Look for infinite retry loops
  2. Not using session-aware delays:

    // Use conditional delays
    backoff := retry.NewConstant(5*time.Second)
    if session != nil {
        backoff = retry.NewConstant(1*time.Millisecond)
    }
    

Problem: Recording Proxy Fails to Start

Error:

failed to create proxy client: ...

Solutions:

  1. Port already in use:

    • Proxy uses random port, should be rare
    • Check for other tests running in parallel
  2. Certificate issues:

    • Ensure -tags=record build accepts self-signed certs
    • Check cmd/deps_record.go configuration

Advanced Topics

Custom Matchers

Add custom request matching logic:

// In your test or in recording.go
vcr.SetMatcher(func(r *http.Request, i cassette.Request) bool {
    // Custom matching logic
    if strings.Contains(r.URL.Path, "/special-resource/") {
        // Ignore resource ID in matching
        return r.Method == i.Method && 
               matchesWithoutId(r.URL.Path, i.URL)
    }
    
    return cassette.DefaultMatcher(r, i)
})

Host Mapping for Test Servers

When using httptest.NewServer:

server := httptest.NewServer(handler)
defer server.Close()

session := recording.Start(t, 
    recording.WithHostMapping(
        strings.TrimPrefix(server.URL, "http://"), 
        "localhost:8080"))

Fixed Clock for Time-Dependent Tests

// Set environment variable
os.Setenv("AZD_TEST_FIXED_CLOCK_UNIX_TIME", "1744738873")

// azd will use fixed time in recording mode
// Useful for deployment name generation

Summary Checklist

Creating a New Test ✓

  • Start with recording.Start(t)
  • Use randomOrStoredEnvName(session)
  • Create CLI with azdcli.WithSession(session)
  • Set a private AZD_CONFIG_DIR (plus AZURE_DEV_COLLECT_TELEMETRY=no) on cli.Env for user-level state changes; leave it unset when the test needs live Azure access
  • Use session.ProxyClient for HTTP operations
  • Store dynamic values in session.Variables
  • Use tempDirWithDiagnostics(t) and register other cleanup immediately
  • Use appropriate timeouts for recording/playback
  • Guard fixture type assertions so malformed data fails the test without panicking

Recording a Test ✓

  • Set AZURE_RECORD_MODE=record
  • Ensure Azure authentication is configured
  • Run test: go test -v -run ^TestName$ ./test/functional -timeout 30m
  • Verify recordings created in testdata/recordings/
  • Commit recordings to git

Debugging Recording Issues ✓

  • Enable debug logging: RECORDER_PROXY_DEBUG=1
  • Check session mode: t.Logf("Playback: %v", session.Playback)
  • Verify HTTP client usage
  • Check session variable storage
  • Review sanitization for sensitive data

CI Integration ✓

  • Build with -BuildRecordMode for test binaries
  • Commit recordings to repository
  • Set AZURE_RECORD_MODE=playback in CI (optional)
  • Ensure recordings are up to date

Resources

  • Recording Package: cli/azd/test/recording/
  • Command Recording: cli/azd/test/cmdrecord/
  • Test Helpers: cli/azd/test/azdcli/
  • Example Tests: cli/azd/test/functional/up_test.go
  • go-vcr Documentation: https://github.com/dnaeon/go-vcr