What Decision Does This Pester Test Control?
📦 Run it yourself — this post’s examples are in
companion/tests/Unit/Eligibility.Tests.ps1.
Most Pester examples begin with a function.
Production testing should begin with a decision.
A test can be perfectly written, beautifully named, and completely useless if nobody knows what its result changes. Does failure block a pull request? Stop a deployment? Quarantine one batch? Open a ticket? Wake someone at 2:00 a.m.? Or does it merely prove that a developer remembered how Should-Be works?
That last one is not much of a production strategy.
This series will use one question over and over:
What decision does this test control?
What you will learn
- Why “the test passed” is incomplete information
- How to connect a Pester test to a production risk
- The difference between test location, timing, and purpose
- How to document the action taken on failure
- How the Partner Feed Guardian example will work through the series
Meet the Partner Feed Guardian
The running example is a PowerShell system that receives batch files from outside partners.
Each batch contains:
- A partner identifier
- A unique batch identifier
- A schema version
- An effective processing date
- A signed manifest
- A list of expected payload files
- The payload files themselves
The automation must decide whether to accept, reject, quarantine, publish, acknowledge, and reconcile the batch.
This is not a server-health checklist. It is a production workflow with business and operational consequences.
A bad decision could:
- Publish a batch twice
- Accept an unsupported schema
- Process data before its effective date
- Publish half of a coordinated batch
- Send an acknowledgement before publication actually succeeded
- Route a partner’s data to the wrong destination
Pester cannot make those risks disappear. It can help us state what must be true before the next decision is allowed.
A test needs more than a name
Consider this test:
It 'rejects an unsupported schema' {
$result.Status | Should-Be 'Rejected'
}The assertion is clear, but the operational meaning is still missing.
We need to know:
| Question | Answer |
|---|---|
| What risk does it address? | Publishing data the current processor cannot interpret safely |
| Where does it run? | Developer workstation and clean CI runner |
| When does it run? | Every pull request that changes batch rules |
| What may it touch? | In-memory test objects only |
| What does failure do? | Blocks the pull request |
| Who owns it? | The team that owns partner intake rules |
| What does it not prove? | That the real partner endpoint or production publisher is available |
Now the test has a job.
Where, when, and why are different questions
These ideas are often mixed together.
Where it runs
Examples include:
- Developer workstation
- CI runner
- Disposable integration environment
- Canary deployment host
- Production
When it runs
Examples include:
- On every local change
- On every pull request
- Nightly
- Before an operation
- During deployment
- Immediately after deployment
- During an incident
Why it runs
Examples include:
- Protect a decision rule
- Verify a dependency contract
- Stop an unsafe operation
- Confirm a deployment landed
- Detect drift
- Prove recovery
One test can run in several places and at several times. A batch-eligibility test might run locally and in CI. A deployment-verification test might run on a canary and later on the rest of the runner fleet.
That is why this series will not put every test into a single permanent box called “unit,” “integration,” or “production.” Those labels help, but the production decision is the part that gives the test meaning.
Build the first decision function
Our first function does not connect to an SFTP site, publish data, or touch production. It evaluates a batch against known rules.
function Test-PfgBatchEligibility {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[psobject]$Batch,
[Parameter(Mandatory)]
[string[]]$SupportedSchemas,
[string[]]$ProcessedBatchIds = @(),
[bool]$PartnerApproved = $true,
[datetime]$Now = (Get-Date)
)
$reasons = [System.Collections.Generic.List[string]]::new()
if (-not $PartnerApproved) {
$reasons.Add('PartnerNotApproved')
}
if ($Batch.SchemaVersion -notin $SupportedSchemas) {
$reasons.Add('UnsupportedSchema')
}
if ($Batch.BatchId -in $ProcessedBatchIds) {
$reasons.Add('DuplicateBatch')
}
if ([datetime]$Batch.EffectiveDate -gt $Now) {
$reasons.Add('EffectiveDateNotReached')
}
[pscustomobject]@{
BatchId = $Batch.BatchId
Status = if ($reasons.Count -eq 0) { 'Approved' } else { 'Rejected' }
Reasons = @($reasons)
}
}This function is deliberately boring in the best possible way. It makes decisions and returns evidence. It does not publish anything.
That separation gives us a fast test that can run repeatedly without a lab, credentials, or cleanup.
Write the first production-oriented test
BeforeAll {
Import-Module "$PSScriptRoot/../../src/PartnerFeedGuardian.psd1" -Force
}
Describe 'Partner batch eligibility' -Tag 'Layer.Unit', 'Gate.PullRequest' {
It 'rejects an unsupported schema' {
$batch = New-PfgBatch `
-PartnerId 'NORTHWIND' `
-BatchId 'NW-20260717-001' `
-SchemaVersion '9.0' `
-EffectiveDate ([datetime]'2026-07-17') `
-Payloads @('orders.json') `
-ManifestPayloads @('orders.json')
$result = Test-PfgBatchEligibility `
-Batch $batch `
-SupportedSchemas @('2.0', '2.1') `
-Now ([datetime]'2026-07-17T12:00:00Z')
$result.Status | Should-Be 'Rejected'
$result.Reasons | Should-ContainCollection @('UnsupportedSchema')
}
}Notice a few choices.
The clock is controlled
The test passes a fixed -Now value. It does not depend on the day someone happens to run it.
A test involving dates that calls Get-Date internally without a controllable boundary often becomes a future failure waiting patiently for a holiday or midnight.
The output explains the decision
Rejected is useful. UnsupportedSchema is more useful.
Production automation should return enough information for the next layer to decide whether to block, quarantine, retry, or escalate.
The tags describe use, not just technology
-Tag 'Layer.Unit', 'Gate.PullRequest'Layer.Unit describes the test’s isolation.
Gate.PullRequest describes the decision it controls.
That is more useful than a generic tag such as Fast or Test1.
Make the policy explicit
A production test record can be stored beside the suite as Markdown, YAML, JSON, or metadata in your test-management process.
For this test:
name: Reject unsupported partner schema
risk: Unsupported input is published by a processor that cannot interpret it safely
location: CI runner
identity: Pipeline service identity
when: Pull request
access: In-memory objects only
failureAction: Block merge
owner: Partner Intake Engineering
proves: Eligibility logic rejects a schema outside the approved list
limitations:
- Does not contact the real partner endpoint
- Does not prove the production publisher is healthy
- Does not prove the approved schema list is correctThat last limitation is important.
The test proves that the code obeys the supplied list. It does not prove that the list itself reflects the right business decision. Tests can faithfully enforce a bad requirement.
Not every failure should stop everything
Imagine three failing tests:
- An unsupported schema is accepted.
- A nonessential report label contains the wrong capitalization.
- One production partner’s routing configuration differs from the approved configuration.
All are failures. They should not all have the same response.
A sensible policy might be:
| Failure | Action |
|---|---|
| Unsupported schema accepted | Block merge and deployment |
| Report label capitalization | Fail the report test; fix during normal development |
| Partner route drift | Suspend that partner’s next batch and open an urgent ticket |
Pester gives you test results. Your operating model decides what they mean.
A gate must recognize incomplete execution
A required test that did not run is not a passing test.
For informational suites, skipping may be acceptable. For a preflight or deployment gate, skipped, inconclusive, and not-run tests should normally stop the operation until someone understands why.
Later in the series we will use the Pester result object to enforce that policy:
$result = Invoke-Pester -Configuration $config
$gateFailed =
$result.FailedCount -gt 0 -or
$result.SkippedCount -gt 0 -or
$result.InconclusiveCount -gt 0 -or
$result.NotRunCount -gt 0 -or
$result.FailedBlocksCount -gt 0 -or
$result.FailedContainersCount -gt 0
if ($gateFailed) {
throw 'The required gate did not complete successfully.'
}A green summary with three skipped prerequisites should not grant permission to change production.
What this test proves
The unsupported-schema test proves that, given the supplied batch and supported-schema list, the eligibility function returns a rejection containing the expected reason.
It provides evidence about the rule.
What it does not prove
It does not prove:
- The schema list came from the right source
- The real manifest parser extracts the version correctly
- The partner supplied the file it claims to have supplied
- The production identity can read the batch
- Publication is safe
- The deployment contains the tested code
Those are different claims and require different tests.
The purpose of a layered strategy is not to make one enormous test prove everything. It is to make each test prove one useful thing at the least expensive and safest layer available.
Try it yourself
Pick one PowerShell script that can affect production.
Write down one decision it makes. Not the command it runs—the decision.
Examples:
- Is this request eligible to proceed?
- Should this item be quarantined?
- Which approval level is required?
- Is this retry safe?
- Has this operation already completed?
Then complete this record:
Risk:
Test claim:
Where it runs:
When it runs:
Identity:
What it may touch:
Failure action:
Owner:
What it proves:
What it does not prove:
Only after that should you write the It block.
Common mistakes
Starting with the cmdlet instead of the decision. “Test
Invoke-RestMethod” is not a useful goal. Decide what result or behavior matters.
Giving every test the same failure action. Not every red result should block a fleet-wide deployment or page an operator.
Confusing a mocked test with dependency evidence. A mocked response proves how your code handles that response, not that the real system returns it.
Leaving ownership blank. A scheduled test without an owner is future alert noise.
Claiming more than the assertion proves. A clean unit suite does not prove the deployed system is healthy.
Recap
A production-facing Pester test needs a technical assertion and an operational purpose.
Before writing it, define:
- The risk
- The claim
- The location
- The timing
- The identity
- The permitted access
- The failure action
- The owner
- The limitation
Next up: Part 2 — Design PowerShell That Can Be Tested and Stopped. We will restructure automation into inspect, decide, plan, approve, apply, and verify stages—then test that -WhatIf actually prevents the state-changing dependency from being called.