What Decision Does This Pester Test Control?

PesterForge · July 2026 · 9 min read

📦 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

Meet the Partner Feed Guardian

The running example is a PowerShell system that receives batch files from outside partners.

Each batch contains:

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:

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:

When it runs

Examples include:

Why it runs

Examples include:

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 correct

That 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:

  1. An unsupported schema is accepted.
  2. A nonessential report label contains the wrong capitalization.
  3. 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:

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:

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:

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.


← All posts