Safe Production Tests and Synthetic Transactions with Pester

PesterForge · July 2026 · 8 min read

📦 Run it yourself — production suites are isolated in companion/tests/Production/, and pester.production.ps1 refuses to load unless an explicit production switch is present.

Sooner or later, a staging environment stops being convincing.

Production has the real identities, routing, data scale, version mix, policy, latency, and integrations. A test environment can imitate those things, but it rarely reproduces all of them at once.

That does not mean we should turn Pester loose in production with administrator rights and a cheerful sense of adventure.

It means production tests need a stronger safety model.

What you will learn

Three levels of production testing

Level 1: Read-only observation

The test queries production state without changing it.

Examples in this series include:

Read-only is the default because it has the smallest blast radius.

Level 2: Isolated synthetic transaction

The test creates clearly marked test data through the normal production path.

Examples:

The transaction must be excluded from normal business metrics, billing, customer communication, and operational reports.

Level 3: Controlled state-changing verification

The test makes a narrowly scoped and reversible change against an isolated target.

Examples:

This level requires the strongest approval, isolation, evidence, and recovery controls.

The synthetic help-desk ticket

Before we use the Partner Feed Guardian, consider a simpler production path.

A test account creates a ticket with:

The test then:

  1. Creates the ticket.
  2. Retrieves it through the normal search path.
  3. Assigns it to a synthetic queue.
  4. Adds a synthetic note.
  5. Closes it.
  6. Confirms the expected audit events.
  7. Removes it or leaves it for the documented retention process.

That proves more than a generic endpoint check. It exercises the production workflow while remaining isolated from real customers.

The Partner Feed Guardian synthetic batch

The Partner Feed Guardian uses a dedicated production partner:

PartnerId: PFG-SYNTHETIC
Destination: synthetic-only
Reporting: excluded
MaximumBatchSize: 3
Retention: 24 hours
Notification: disabled

A synthetic batch should be unmistakable:

$batchId = 'PFG-SYN-{0:yyyyMMddHHmmss}-{1}' -f `
    (Get-Date).ToUniversalTime(), `
    ([guid]::NewGuid().Guid.Substring(0, 8))

The manifest and payloads contain no real customer data.

Guard execution before Pester starts

The production configuration should refuse to load unless the caller opts in explicitly:

if ($env:PFG_ALLOW_PRODUCTION_TESTS -ne 'YES') {
    throw 'Production tests require PFG_ALLOW_PRODUCTION_TESTS=YES.'
}

That is one guard, not the whole safety system.

Add checks for:

Example:

BeforeAll {
    if ($env:PFG_ALLOW_PRODUCTION_TESTS -ne 'YES') {
        throw 'Production test switch is missing.'
    }

    $context = Get-PfgTargetContext

    if ($context.EnvironmentId -ne $expectedProductionId) {
        throw "Unexpected environment: $($context.EnvironmentId)"
    }

    if ($context.Identity -ne 'CONTOSO\svc-pfg-synthetic') {
        throw "Unexpected identity: $($context.Identity)"
    }
}

Never infer production from a friendly name alone. Use stable identifiers where possible.

Require synthetic identifiers at command boundaries

The production adapter should reject non-synthetic targets when called by the synthetic suite.

function Assert-PfgSyntheticBatch {
    [CmdletBinding()]
    param([Parameter(Mandatory)][psobject]$Batch)

    if ($Batch.PartnerId -ne 'PFG-SYNTHETIC') {
        throw 'Synthetic test attempted to use a non-synthetic partner.'
    }

    if ($Batch.BatchId -notlike 'PFG-SYN-*') {
        throw 'Synthetic batch id does not have the required prefix.'
    }

    if ($Batch.Payloads.Count -gt 3) {
        throw 'Synthetic batch exceeds the approved operation limit.'
    }
}

This guard belongs in the production-testing adapter, not only in the test file. A future test edit should not be able to bypass the boundary accidentally.

The production test

Describe 'Synthetic partner batch' `
    -Tag 'Environment.Production', 'Risk.StateChanging', 'Gate.Production' {

    BeforeAll {
        $correlationId = [guid]::NewGuid().Guid
        $batch = New-PfgSyntheticBatch `
            -CorrelationId $correlationId

        Assert-PfgSyntheticBatch -Batch $batch
    }

    It 'travels through the production path and reconciles' {
        $result = Invoke-PfgSyntheticBatch `
            -Batch $batch `
            -CorrelationId $correlationId `
            -Confirm:$false

        $result.Status | Should-Be 'Completed'

        $verification = Get-PfgSyntheticBatchResult `
            -BatchId $batch.BatchId `
            -CorrelationId $correlationId

        $verification | Should-BeEquivalent ([pscustomobject]@{
            BatchId       = $batch.BatchId
            CorrelationId = $correlationId
            Published     = $true
            Acknowledged  = $true
            Reconciled    = $true
            Destination   = 'synthetic-only'
        }) -ExcludePathsNotOnExpected
    }
}

The test uses the normal production path but a dedicated production test identity, partner, destination, data set, and reporting exclusion.

Do not use -WhatIf as the production probe

-WhatIf remains valuable for confirming that guarded code does not intentionally change state.

It does not exercise the production path.

A synthetic transaction exists because we need evidence that the actual production identity, routing, publication, acknowledgement, and reconciliation work together.

That evidence requires a controlled state change.

Verify cleanup, but assume teardown can fail

You may use AfterAll:

AfterAll {
    Remove-PfgSyntheticBatch `
        -BatchId $batch.BatchId `
        -CorrelationId $correlationId `
        -Confirm:$false
}

But the PowerShell process can terminate before AfterAll runs.

The system also needs an independent cleanup process:

Get-PfgSyntheticBatch `
    -Prefix 'PFG-SYN-' `
    -OlderThan (New-TimeSpan -Hours 24) |
    Remove-PfgSyntheticBatch -Confirm:$false

The cleanup process should be tested separately and owned explicitly.

Make synthetic activity visible

Synthetic records should include:

Hidden test data is not safer. It is simply harder to distinguish from a real incident.

Limit frequency and volume

A production probe that runs every minute can become production load.

Set:

A test should not continue hammering a failing dependency to prove that it remains failed.

Define failure actions by stage

Failure Action
Guard condition fails Do not create synthetic state; alert test owner
Creation fails before accepted Record failure; no cleanup expected
Result unknown after submission Reconcile by correlation ID before retry
Published but not acknowledged Preserve state; retry acknowledgement only
Reconciliation fails Open urgent ticket; preserve synthetic batch
Cleanup fails Open cleanup ticket with exact resource identifiers

The result should not simply say Production test failed.

Protect secrets and output

Pester captures output. Test-result files can be retained or uploaded.

Never place tokens, private keys, full connection strings, or sensitive production payloads in:

Use safe identifiers and redacted diagnostics.

Keep production tests sequential by default

The Pester 6 parallel runner is experimental and operates at the test-file level.

Production tests should remain sequential unless isolation and provider capacity have been deliberately proven.

Two individually safe synthetic tests can still collide when they share:

Speed is not the first production-testing objective.

What a synthetic transaction proves

It can prove that a controlled transaction used the real production identity and path and reached the expected observable final state at a specific time.

What it does not prove

It does not prove every customer, partner, payload size, data shape, or load pattern works. It is one carefully designed production sample, not universal proof.

Try it yourself

Design one synthetic transaction and document:

Dedicated identity:
Dedicated target or tenant:
Required prefix:
Maximum operations:
Data classification:
Reporting exclusions:
Correlation method:
Verification query:
Cleanup command:
Independent stale-state cleanup:
Owner:
Failure action:

Do not execute it in production until every field has a real answer.

Common mistakes

Calling a normal business transaction “synthetic” because the name starts with TEST. Isolation must exist in routing, identity, reporting, and cleanup.

Using AfterAll as the only cleanup mechanism. The host may never reach teardown.

Hiding synthetic records. Mark them clearly and make them easy to find.

Running the suite with broad administrator rights. Use the least-privileged dedicated test identity.

Automatically retrying an unknown state-changing outcome. Reconcile first.

Recap

Production tests can change state safely only when the state is dedicated, identifiable, limited, reversible, independently cleanable, and owned.

Read-only remains the default. Synthetic transactions are justified when they prove a production path that cannot be established elsewhere.

Next up: Part 9 — Pester, Drift Detection, Monitoring, and Alerts. We will define the line between evaluating an expectation and operating a monitoring platform—and prevent scheduled Pester checks from becoming ignored noise.


← All posts