The Fast Gate: Decisions, Boundaries, and Regression Tests

PesterForge · July 2026 · 9 min read

📦 Run it yourself — this post’s examples are in companion/tests/Unit/ and companion/config/pester.unit.ps1.

The test suite that protects production most often should be the least dramatic one.

It should not require a VPN, a test tenant, a secret, an available DBA, or a carefully prepared server. It should run on a developer’s machine and on a clean CI worker. It should finish quickly enough that nobody needs an excuse to skip it.

This is the fast gate.

Its job is not to prove that production works. Its job is to stop code with known-bad decisions, broken boundaries, or returned defects from moving closer to production.

What you will learn

What belongs in the fast gate

A strong fast gate usually includes:

It usually does not include:

The gate must be dependable. If it fails randomly because a third-party sandbox is having a bad morning, developers will stop trusting the result.

Test the decision table

The Partner Feed Guardian has several eligibility rules. We can represent the important cases as data:

It 'rejects <Reason>' -ForEach @(
    @{
        Reason         = 'an unsupported schema'
        ExpectedReason = 'UnsupportedSchema'
        SchemaVersion  = '9.0'
        Processed      = @()
        PartnerApproved = $true
    }
    @{
        Reason         = 'a duplicate batch id'
        ExpectedReason = 'DuplicateBatch'
        SchemaVersion  = '2.1'
        Processed      = @('NW-20260717-001')
        PartnerApproved = $true
    }
    @{
        Reason         = 'an unapproved partner'
        ExpectedReason = 'PartnerNotApproved'
        SchemaVersion  = '2.1'
        Processed      = @()
        PartnerApproved = $false
    }
) {
    $batch = New-PfgBatch `
        -PartnerId 'NORTHWIND' `
        -BatchId 'NW-20260717-001' `
        -SchemaVersion $SchemaVersion `
        -EffectiveDate ([datetime]'2026-07-17') `
        -Payloads @('orders.json') `
        -ManifestPayloads @('orders.json')

    $result = Test-PfgBatchEligibility `
        -Batch $batch `
        -SupportedSchemas @('2.0', '2.1') `
        -ProcessedBatchIds $Processed `
        -PartnerApproved $PartnerApproved `
        -Now ([datetime]'2026-07-17T12:00:00Z')

    $result.Status | Should-Be 'Rejected'
    $result.Reasons | Should-ContainCollection @($ExpectedReason)
}

The cases describe operational conditions. They do not repeat the function line by line.

That distinction matters. A test that reimplements the same algorithm may reproduce the same mistake and congratulate the code for agreeing with itself.

Test the edges where systems meet

Production automation often fails at boundaries rather than in its central logic.

Examples include:

Pester 6’s collection and equivalence assertions are useful here.

It 'returns a stable rejection contract' {
    $result = Test-PfgBatchEligibility @testParameters

    $expected = [pscustomobject]@{
        BatchId            = 'NW-1042'
        Status             = 'Rejected'
        Reasons            = @('UnsupportedSchema')
        MissingPayloads    = @()
        UnexpectedPayloads = @()
    }

    $result | Should-BeEquivalent $expected
}

Use a complete-object assertion only when the complete object is the contract you intend to protect.

If timestamps or diagnostic fields can change without breaking the caller, exclude them or assert only the required subset:

$result | Should-BeEquivalent $expected -ExcludePathsNotOnExpected

Test command metadata when callers depend on it

A production script can break callers without changing its internal logic. Removing a parameter alias, changing a type, or making an optional parameter mandatory can be a real compatibility failure.

It 'keeps Batch mandatory and strongly typed' {
    Get-Command Invoke-PfgBatch |
        Should-HaveParameter -ParameterName Batch -Type ([psobject]) -Mandatory
}

Do not test every parameter merely because Pester can. Protect the public surface that callers actually rely upon.

Use mocks to isolate orchestration

Suppose Invoke-PfgBatch must publish before it acknowledges.

It 'publishes before acknowledging' {
    $script:callOrder = [System.Collections.Generic.List[string]]::new()

    Mock -ModuleName PartnerFeedGuardian Get-PfgOperationState { 'NotStarted' }
    Mock -ModuleName PartnerFeedGuardian Set-PfgOperationState { }
    Mock -ModuleName PartnerFeedGuardian Publish-PfgBatch {
        $script:callOrder.Add('Publish')
    }
    Mock -ModuleName PartnerFeedGuardian Send-PfgAcknowledgement {
        $script:callOrder.Add('Acknowledge')
    }
    Mock -ModuleName PartnerFeedGuardian Write-PfgEvidence { }

    Invoke-PfgBatch `
        -Batch $batch `
        -Eligibility $approved `
        -Confirm:$false

    $script:callOrder | Should-BeCollection @('Publish', 'Acknowledge')
}

This is a good mock use: isolate external actions and verify our orchestration.

It does not prove that publication or acknowledgement works. That is not the claim.

Add default mocks for dangerous dependencies

Pester 6 no longer falls through to the real command when no filtered mock matches, which is safer. I still prefer an explicit throwing default for especially dangerous boundaries because the failure message can explain the problem immediately.

Mock -ModuleName PartnerFeedGuardian Publish-PfgBatch {
    throw "Unexpected publication call for BatchId '$($Batch.BatchId)'"
}

Mock -ModuleName PartnerFeedGuardian Publish-PfgBatch {
    # Expected test behavior
} -ParameterFilter {
    $Batch.BatchId -eq 'TEST-1042'
}

Now an unexpected target produces an intentional test failure rather than a vague unmatched-mock message.

Do not assert private choreography without a reason

Mock assertions are powerful and easy to overuse.

This test may be brittle:

Should-Invoke Get-PfgConfiguration -Times 1 -Exactly

Why must it be exactly once? If the function safely caches or validates the configuration twice, did production behavior break?

Call assertions are most valuable when the call itself is the behavior:

Test consequences, not every internal footstep.

Create regression tests from real failures

A regression test is a receipt from an incident.

Suppose production accepted a batch with schema 2.1-preview because the code used a wildcard:

if ($Batch.SchemaVersion -like '2.1*') {
    # approved
}

The immediate fix is useful. The regression test is what prevents the same class of shortcut from returning later:

It 'rejects preview schema names even when the supported version is 2.1' {
    $batch = New-PfgBatch `
        -PartnerId 'NORTHWIND' `
        -BatchId 'REGRESSION-2417' `
        -SchemaVersion '2.1-preview' `
        -EffectiveDate ([datetime]'2026-07-17') `
        -Payloads @('orders.json') `
        -ManifestPayloads @('orders.json')

    $result = Test-PfgBatchEligibility `
        -Batch $batch `
        -SupportedSchemas @('2.1') `
        -Now ([datetime]'2026-07-17T12:00:00Z')

    $result.Status | Should-Be 'Rejected'
    $result.Reasons | Should-ContainCollection @('UnsupportedSchema')
}

Name the incident or defect in a comment or test metadata so future maintainers understand why the odd-looking case matters.

Configure the gate explicitly

The companion repository uses a dedicated unit configuration:

Import-Module Pester -RequiredVersion 6.0.0

$config = New-PesterConfiguration
$config.Run.Path = "$PSScriptRoot/../tests/Unit"
$config.Run.PassThru = $true
$config.Output.Verbosity = 'Detailed'
$config.Should.DisableV5 = $true
$config.Filter.Tag = @('Layer.Unit')
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = "$PSScriptRoot/../artifacts/unit-results.xml"
$config.TestResult.OutputFormat = 'NUnitXml'
$config.Run.Exit = $true

The configuration is version-controlled. Developers and CI use the same policy.

That is more reliable than a README saying, “Run Pester with the right flags.”

Treat incomplete runs as failed gates

Pester’s overall run result can be Passed even though some tests were skipped intentionally. For a required gate, inspect the counts.

$result = Invoke-Pester -Configuration $config

$problems = @{
    Failed       = $result.FailedCount
    Skipped      = $result.SkippedCount
    Inconclusive = $result.InconclusiveCount
    NotRun       = $result.NotRunCount
    Blocks       = $result.FailedBlocksCount
    Containers   = $result.FailedContainersCount
}

if ($problems.Values | Where-Object { $_ -gt 0 }) {
    throw "Pull-request gate incomplete: $($problems | ConvertTo-Json -Compress)"
}

Your policy may permit explicitly marked optional tests to skip. Then separate them from required gates rather than weakening the required-gate rule until it means nothing.

Keep the fast gate fast

There is no universal time limit, but the suite should feel like a normal part of development.

If it takes twenty minutes because it launches remote sessions and waits on shared resources, it is no longer the fast gate. Move those checks into an integration profile.

Speed is not the only concern. Determinism matters more. A six-second suite that fails randomly is worse than a thirty-second suite that tells the truth.

What the fast gate proves

It can prove that:

What it does not prove

It does not prove:

Those claims come next.

Try it yourself

Build a fast gate for one script:

  1. Identify three high-impact decisions.
  2. Add one boundary or output-contract test.
  3. Add one test proving a dangerous dependency is not called.
  4. Convert one historical defect into a regression test.
  5. Put the suite behind a version-controlled Pester configuration.
  6. Fail the gate when required tests are skipped or not run.

Common mistakes

Putting live infrastructure in every pull request. Keep the fast gate independent and deterministic.

Reimplementing the production algorithm in the test. Use clear input/output cases rather than a second copy of the same logic.

Mocking everything and claiming integration coverage. Mocks isolate; they do not verify the provider.

Counting every internal call. Assert calls when their occurrence, absence, order, or target protects production behavior.

Allowing skipped required tests to look green. A gate that did not run is not permission to proceed.

Recap

The fast gate protects production by stopping bad code early and cheaply.

It should focus on decisions, boundaries, public contracts, dangerous-call prevention, and regressions. It should be deterministic, version-controlled, and strict about incomplete execution.

Next up: Part 4 — The Real Dependency and the Real Identity. We will leave the comfort of mocks and test the conditions that exist only when the actual adapter, account, module path, and noninteractive session are involved.

Technical references


← All posts