Safe Production Tests and Synthetic Transactions with Pester
📦 Run it yourself — production suites are isolated in
companion/tests/Production/, andpester.production.ps1refuses 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-facing tests
- When read-only inspection is not enough
- How to design an isolated synthetic transaction
- What safeguards should exist before Pester can change production state
- Why test teardown is not a complete cleanup strategy
- How to prevent accidental execution against production
Three levels of production testing
Level 1: Read-only observation
The test queries production state without changing it.
Examples in this series include:
- Confirming the deployed module version
- Comparing routing configuration to the approved source
- Verifying that a completed synthetic batch reconciled correctly
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:
- A dedicated synthetic partner batch
- A help-desk ticket in a synthetic queue
- A test message through a production messaging path
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:
- Toggle a test-only feature flag
- Apply and remove a route on a synthetic partner
- Rotate a test credential in a dedicated test identity
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:
- Category:
Synthetic Validation - Requester: dedicated synthetic identity
- Unique marker:
PSTER-SYN-<guid> - Reporting exclusion: true
- Automatic expiration: 24 hours
The test then:
- Creates the ticket.
- Retrieves it through the normal search path.
- Assigns it to a synthetic queue.
- Adds a synthetic note.
- Closes it.
- Confirms the expected audit events.
- 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:
- Expected tenant or environment ID
- Approved production test identity
- Synthetic partner ID
- Maximum operation count
- Synthetic prefix
- Current change or approval record
- Independent cleanup command
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:$falseThe cleanup process should be tested separately and owned explicitly.
Make synthetic activity visible
Synthetic records should include:
- Batch or transaction ID
- Correlation ID
- Test suite and version
- Created time
- Expiration time
- Owner
- Cleanup status
- Business-reporting exclusion
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:
- Maximum transactions per run
- Maximum runs per hour
- Payload-size limits
- Concurrency limits
- Provider-specific rate limits
- Automatic shutoff after repeated failures
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:
- Test names
- Assertion messages
Write-Host- Exception text
- NUnit or JUnit properties
- Captured standard output
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:
- One synthetic account
- One queue
- One rate limit
- One cleanup process
- One mutable configuration object
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
AfterAllas 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.