The Fast Gate: Decisions, Boundaries, and Regression Tests
📦 Run it yourself — this post’s examples are in
companion/tests/Unit/andcompanion/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 a fast pull-request gate
- How to test decision tables without copying the implementation
- How to protect data and configuration boundaries
- How Pester 6 mocks should be used safely
- How to turn an incident into a regression test
- How to reject incomplete Pester runs
What belongs in the fast gate
A strong fast gate usually includes:
- Decision logic
- Parameter and command-shape checks
- Output contracts
- Configuration-schema validation
- Boundary cases
- Regression tests
- Packaging and module-import checks
- Mocked orchestration behavior
It usually does not include:
- Real production credentials
- A live partner endpoint
- State-changing cloud operations
- A shared lab that is frequently unavailable
- Long waits for eventual consistency
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:
- A configuration key is renamed
- A manifest field changes type
- A command returns one object instead of an array
- A downstream object loses a property
- A date includes an unexpected offset
- An empty payload list becomes
$null
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 -ExcludePathsNotOnExpectedTest 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 -ExactlyWhy 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:
- A state-changing command must not run
- A publication must occur only once
- An acknowledgement must follow publication
- An operation must use a constrained target
- A retry must not repeat an irreversible action
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 = $trueThe 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:
- Decision rules behave for known cases
- Public output maintains its contract
- Unsafe dependency calls are blocked in tested paths
- Orchestration follows important rules
- Known defects remain fixed
- The module packages and imports as expected
What it does not prove
It does not prove:
- Real authentication works
- The partner endpoint behaves as mocked
- Production has the tested module version
- The service identity sees the same module path
- The deployment is healthy
- The production configuration matches source control
Those claims come next.
Try it yourself
Build a fast gate for one script:
- Identify three high-impact decisions.
- Add one boundary or output-contract test.
- Add one test proving a dangerous dependency is not called.
- Convert one historical defect into a regression test.
- Put the suite behind a version-controlled Pester configuration.
- 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.