Turn "It Worked When I Ran It" into a Test Suite That Catches the Regression Before Production Does
An AI-assisted workflow to generate a safe, behavior-focused Pester 5 test suite for any PowerShell script — mocking every destructive command so tests never touch production.
Executive Brief
Your Challenge
Your script works when you run it by hand in one environment — and that proves nothing about whether it still works after a module update, an OS upgrade, or the next code change. Without automated tests, a one-line edit can quietly introduce a regression that stays invisible until a production task fails at the worst possible time. You know you should have Pester tests; the reason you don't is that writing them feels like it takes longer than the script did.
Common Obstacles
The trap with AI-generated tests is that they validate the code that exists rather than the behavior you intended — if the script has a bug, the generated test faithfully confirms the bug. The other failure is safety: a test suite that calls Remove-ADUser or Restart-Service against a real domain is not a unit-test suite, it's an outage waiting to run. And tests that can never fail — Should -Not -BeNullOrEmpty on the wrong records — give you the comfort of coverage with none of the protection.
The ABME Approach
This workflow does it in the right order: gather the script and its intended behavior, then use the prompt pack to generate Pester 5 tests that mock every external and state-changing command, then validate the suite against a 16-point acceptance checklist. You supply the intended requirement separately from the source so the AI tests what the script should do, not just what it does. Every destructive command is mocked, time-dependent logic is frozen with a mocked Get-Date, and integration tests are tagged and excluded from ordinary runs.
Insight Summary
A script that passes tests is only as trustworthy as what the tests assert. Coverage measures how much code ran, not whether the right thing happened.
Describe what the script should do, not only what the code currently does — AI infers expected behavior from existing code, and if the code is wrong the test just confirms the bug.
A test suite that invokes real administrative commands is not a safe unit-test suite. Mock every command that creates, updates, disables, deletes, or restarts a resource before running anything.
Mock system boundaries and external dependencies, not every line of code. Mocking every internal function makes tests brittle and stops meaningful logic from ever being exercised.
A test that can never fail is worse than no test — it manufactures confidence. Confirm each test fails when the relevant production behavior is deliberately broken.
Freeze time before you assert on it: mock Get-Date so inactivity, expiry, and maintenance-window logic produces the same result on every run.
The Journey
Three phases; each lists the tools you'll use there.
Define Intended Behavior and Dependencies
- Gather the complete script or module and its intended purpose
- Document expected inputs, outputs, and known error conditions
- List required modules and external systems accessed
- Confirm supported PowerShell and Pester versions
- Describe what the script should do, separately from what the code does
Generate the Test Suite with AI
- Run the primary prompt with the script and its intended behavior
- Apply follow-up prompts for coverage, mocking, parameter validation, and WhatIf testing
- Confirm every state-changing and external command is mocked
- Separate unit tests from integration-test recommendations
- Generate CI/CD configuration to run the suite
Validate and Operationalize
- Run the 16-point validation checklist
- Confirm tests validate requirements rather than implementation details
- Verify tests fail when the underlying behavior is intentionally broken
- Tag and exclude integration tests from unit runs
- Wire the suite into a CI/CD pipeline
What's Inside the Execution Layer
Numbered deliverables grouped by phase. Membership unlocks every tool.
Prerequisites Checklist
- Collect the script and its intended behavior before prompting
- Surface external dependencies that must be mocked
- Confirm the target PowerShell and Pester versions
Before using the workflow, gather:
Pester Test Generation Prompt Pack
- Generate a Pester 5 test strategy and suite that mocks every dependency
- Deepen coverage of parameters, WhatIf behavior, and edge cases
- Produce CI/CD configuration that fails the build on test failure
Primary Prompt
Start here with the full script or module and a description of its intended behavior.You are a senior PowerShell developer and test engineer with expertise in Pester 5.I will provide a PowerShell script or module and a description of its intended behavior.Analyze the code and create a production-quality Pester test strategy and test suite.Before generating tests:1. Summarize the purpose of the script.2. Identify each function, parameter, and major execution path.3. Identify observable behaviors that should be tested.4. Identify external dependencies.5. Identify commands, APIs, modules, files, services, registry keys, or remote systems that should be mocked.6. Identify destructive or state-changing operations.7. Identify areas that are difficult to test and explain why.8. Identify any missing business requirements required to write meaningful tests.Then generate Pester 5-compatible tests that include, where applicable:• Describe blocks organized by function or behavior• Context blocks for different scenarios• BeforeAll, BeforeEach, AfterEach, or AfterAll setup• Parameter validation tests• Expected success-path tests• Expected failure-path tests• Edge-case tests• Mock definitions• Assert-MockCalled validation• Output-type validation• Return-value validation• Exception validation• Tests for -WhatIf and -Confirm behavior• Tests that prevent real production changes• Clear test names written as behavioral expectationsRequirements:• Do not call real production systems.• Mock all state-changing or external commands.• Do not invent expected behavior without identifying it as an assumption.• Use Pester 5 syntax unless I specify otherwise.• Separate unit tests from integration-test recommendations.• Explain what each test validates.• Flag any test that requires environment-specific configuration.• Recommend code changes only when necessary to improve testability.After the test suite, provide:1. Instructions for saving and running the tests.2. Expected test results.3. Remaining coverage gaps.4. Integration tests that should be performed separately.5. Recommendations for adding the tests to CI/CD.
Improve Test Coverage
After the initial suite, to close gaps without duplicating coverage.Review the proposed Pester tests and identify important execution paths, parameters, errors, or edge cases that are not yet covered. Add the missing tests without duplicating existing coverage.
Create Tests Before Refactoring
Before refactoring undocumented legacy scripts.Create characterization tests that document the script’s current behavior before it is refactored. Do not assume the current behavior is correct; clearly distinguish observed behavior from desired behavior.
Mock External Dependencies
To make the suite safe against real systems.Review the script and create safe Pester mocks for every external dependency, including modules, APIs, Active Directory, Microsoft Graph, Azure, filesystems, registry operations, services, and remote commands.
Test Parameter Validation
To cover mandatory, default, and invalid parameter cases.Generate Pester tests for all parameters, including mandatory parameters, default values, accepted data types, validation attributes, null values, empty strings, invalid values, and pipeline input where applicable.
Add WhatIf Testing
For scripts with state-changing operations.Create Pester tests that confirm the script or function honors SupportsShouldProcess, -WhatIf, and -Confirm before performing state-changing operations.
Create CI/CD Configuration
To wire the suite into a build pipeline.Provide a CI/CD example that installs the required PowerShell and Pester versions, runs the test suite, exports test results, fails the build when tests fail, and optionally enforces a code-coverage threshold.
Improve Testability
When tightly coupled logic makes tests hard to write.Identify portions of the script that are difficult to test because of tightly coupled logic, global state, direct external calls, or code executing outside functions. Recommend the smallest practical changes needed to improve testability.
Validation Checklist
- Accept or reject the AI's generated tests
- Confirm tests validate behavior and cannot touch production
- Verify tests fail when behavior is intentionally broken
Before accepting the generated tests:
🔒 The full execution layer — every checklist, matrix, and the prompt pack — is included with ABME membership.
Unlock Full BlueprintFull Playbook
Overviewpublic
PowerShell scripts are often deployed without automated tests. Administrators may manually confirm that a script works in one environment, but that does not guarantee it will continue working after code changes, module updates, operating-system upgrades, or changes to external services.
This workflow uses AI to analyze an existing PowerShell script and create a structured Pester test suite. The generated tests are designed to validate expected behavior, parameter handling, error conditions, dependencies, and potentially destructive actions without modifying production systems.
The goal is repeatable validation before deployment.
Business Problempublic
PowerShell automation frequently lacks formal testing because:
- Scripts were written for immediate operational needs.
- Administrators are unfamiliar with Pester.
- Testing is performed manually and inconsistently.
- Production dependencies are difficult to reproduce.
- Scripts interact with Active Directory, Microsoft 365, Azure, filesystems, APIs, or remote systems.
- Test creation is perceived as taking longer than the script itself.
- Existing scripts were not designed with testability in mind.
Without automated tests, small changes can create regressions that are not discovered until a production task fails.
Typical Use Casespublic
Use this workflow when:
- Adding tests to an existing PowerShell script
- Preparing automation for production deployment
- Refactoring legacy PowerShell code
- Creating a PowerShell module
- Building a CI/CD pipeline
- Validating parameter behavior
- Testing error handling
- Mocking external services
- Preventing regressions
- Establishing code-quality standards
Do NOT Use This Workflow Whenpublic
This workflow is not intended to:
- Prove that a script is safe solely because tests pass
- Replace integration or user-acceptance testing
- Run destructive commands against production resources
- Generate tests without reviewing the script’s intended behavior
- Test unknown or undocumented business requirements
- Guarantee complete test coverage
AI-generated tests must be reviewed to ensure they validate the correct behavior rather than merely reproducing the current implementation.
Expected Outcomepublic
After completing this workflow, you should have:
- A test strategy
- A list of testable script behaviors
- Identified external dependencies
- Recommended mocking requirements
- Pester unit tests
- Parameter-validation tests
- Success and failure-path tests
- Edge-case tests
- Instructions for running the tests
- Recommendations for improving script testability
🔒 The complete playbook — reference models, worked examples, and operational guidance — is included with ABME membership.
Unlock Full BlueprintExample Inputprotected
function Get-InactiveUser {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[int]$DaysInactive
)
$cutoffDate = (Get-Date).AddDays(-$DaysInactive)
Get-ADUser -Filter * -Properties LastLogonDate |
Where-Object {
$_.Enabled -eq $true -and
$_.LastLogonDate -lt $cutoffDate
}
}Intended behavior:
- Return enabled Active Directory users whose LastLogonDate is older than the specified number of days.
- Do not modify Active Directory.
- Reject invalid values for DaysInactive.
Example Outputprotected
BeforeAll {
. "$PSScriptRoot\Get-InactiveUser.ps1"
}
Describe 'Get-InactiveUser' {
BeforeEach {
Mock Get-Date {
[datetime]'2026-07-21'
}
Mock Get-ADUser {
@(
[pscustomobject]@{
SamAccountName = 'active.user'
Enabled = $true
LastLogonDate = [datetime]'2026-07-15'
}
[pscustomobject]@{
SamAccountName = 'inactive.user'
Enabled = $true
LastLogonDate = [datetime]'2026-01-01'
}
[pscustomobject]@{
SamAccountName = 'disabled.user'
Enabled = $false
LastLogonDate = [datetime]'2025-12-01'
}
)
}
}
It 'returns enabled users older than the inactivity threshold' {
$result = Get-InactiveUser -DaysInactive 90
$result | Should -HaveCount 1
$result.SamAccountName | Should -Be 'inactive.user'
}
It 'does not return active users within the threshold' {
$result = Get-InactiveUser -DaysInactive 90
$result.SamAccountName | Should -Not -Contain 'active.user'
}
It 'does not return disabled users' {
$result = Get-InactiveUser -DaysInactive 90
$result.SamAccountName | Should -Not -Contain 'disabled.user'
}
It 'queries Active Directory once' {
Get-InactiveUser -DaysInactive 90
Should -Invoke Get-ADUser -Times 1 -Exactly
}
}Example Analysisprotected
The test suite freezes the current date using a mocked Get-Date command, allowing the inactivity calculation to produce consistent results on every test run.
Get-ADUser is mocked so the tests do not require Active Directory access and cannot query or modify a production domain. The mocked data includes:
- An enabled user within the inactivity threshold
- An enabled user outside the threshold
- A disabled user outside the threshold
This verifies the two filtering conditions independently.
One issue remains: the function accepts negative or zero values for DaysInactive. The production code should ideally include a validation attribute such as:
[ValidateRange(1, 3650)]
[int]$DaysInactiveTests for invalid values should be added after the intended validation range is confirmed.
Recommended Test Structureprotected
A standard project layout might look like this:
PowerShellProject/
│
├── Public/
│ └── Get-InactiveUser.ps1
│
├── Private/
│
├── Tests/
│ └── Get-InactiveUser.Tests.ps1
│
├── PowerShellProject.psd1
└── PowerShellProject.psm1For a standalone script:
InactiveUsers/
│
├── Get-InactiveUser.ps1
└── Get-InactiveUser.Tests.ps1Running the Testsprotected
Install Pester for the current user:
Install-Module Pester -Scope CurrentUser -ForceRun all tests in the current project:
Invoke-PesterRun one test file:
Invoke-Pester -Path .\Tests\Get-InactiveUser.Tests.ps1Run with detailed output:
Invoke-Pester -Path .\Tests -Output DetailedRun with code coverage:
$config = New-PesterConfiguration
$config.Run.Path = '.\Tests'
$config.Output.Verbosity = 'Detailed'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = '.\Public\*.ps1'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = '.\TestResults.xml'
Invoke-Pester -Configuration $configTest Categoriesprotected
Unit Tests
- Parameter validation
- Filtering logic
- Formatting
- Calculations
- Branching behavior
- Error handling
Integration Tests
- Connecting to Microsoft Graph
- Querying a test Active Directory domain
- Reading from a test SQL database
- Accessing a development API
- Running against a nonproduction Azure subscription
Coverage Guidanceprotected
Recommended Priorities
- Critical business logic
- Destructive operations
- Permission-sensitive functions
- Error handling
- Input validation
- External integrations
- Frequently changed code
- High-impact failure scenarios
Security Considerationsprotected
Pester tests must be designed so they cannot accidentally affect production systems.
Take the following precautions:
- Mock all commands that create, update, disable, delete, restart, revoke, move, or assign resources.
- Never include production credentials in test files.
- Do not embed API keys, access tokens, certificates, or passwords.
- Use synthetic test data.
- Avoid hardcoded production tenant IDs, domain names, subscriptions, servers, or file paths.
- Validate that mock scoping is correct before executing tests.
- Run tests from an isolated build agent or test environment when practical.
- Clearly label integration tests that require live infrastructure.
- Use least-privilege test identities for approved integration testing.
- Prevent test discovery from automatically running unsafe setup code.
Particular caution is required when testing commands such as:
Remove-Item
Remove-ADUser
Disable-ADAccount
Set-ADUser
Restart-Service
Stop-Computer
Invoke-Command
Set-AzContext
Remove-AzResource
Remove-MgUser
Set-MailboxEach should normally be mocked during unit testing.
Common Mistakesprotected
Testing the Implementation Instead of the Behavior
❌ Weak test:
Should -Invoke Where-Object -Times 1This confirms how the function is written, but not whether it returns the correct users.
✅ Better test:
$result.SamAccountName | Should -Be 'inactive.user'This validates the expected outcome.
Writing Tests That Can Never Fail
❌ Weak test:
$result | Should -Not -BeNullOrEmptyThis might pass even if the wrong records are returned.
✅ Better test:
$result | Should -HaveCount 1
$result.SamAccountName | Should -Be 'inactive.user'Reproducing a Bug in the Test
AI may infer expected behavior from existing code. If the code is wrong, the generated test may simply confirm the wrong behavior.
Always provide the intended business requirement separately.
Failing to Mock Destructive Commands
A test suite that invokes real administrative commands is not a safe unit-test suite.
Review every external command before running generated tests.
Overusing Mocks
Mocking every internal function can make tests brittle and prevent meaningful logic from being exercised.
Mock system boundaries and external dependencies, not every line of code.
Ignoring Time-Dependent Behavior
Scripts using Get-Date, token-expiration times, maintenance windows, or age calculations can produce inconsistent test results.
Mock the current time so the tests remain deterministic.
Using Incorrect Pester Syntax
Pester 4 and Pester 5 differ in important ways. Confirm the target version before accepting generated code.
CI/CD Exampleprotected
name: PowerShell Tests
on:
push:
pull_request:
jobs:
test:
runs-on: windows-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install Pester
shell: pwsh
run: |
Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser -Force
- name: Run tests
shell: pwsh
run: |
$config = New-PesterConfiguration
$config.Run.Path = './Tests'
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = './TestResults.xml'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './Public/*.ps1'
$result = Invoke-Pester -Configuration $config
if ($result.FailedCount -gt 0) {
throw "$($result.FailedCount) Pester test(s) failed."
}The exact workflow should be reviewed against current platform and action versions before production use.
Automation Opportunitiesprotected
- GitHub Actions
- Azure DevOps Pipelines
- GitLab CI/CD
- Jenkins
- PowerShell module build pipelines
- Pull-request quality gates
- Internal script repositories
- Automated code-review platforms
- Release-validation workflows
- MSP automation libraries
Pro Tipsprotected
- Give the AI the intended business behavior separately from the source code.
- Start with characterization tests before refactoring undocumented legacy scripts.
- Ask the model to list every external command before it generates mocks.
- Make time-dependent tests deterministic by mocking Get-Date.
- Confirm that each test fails when the relevant production behavior is deliberately broken.
- Use descriptive test names that state the expected behavior.
- Keep unit and integration tests separate.
- Treat code coverage as a diagnostic measure, not the final quality score.
- Require tests for both successful and unsuccessful execution paths.
- Add tests whenever a production defect is discovered so the same issue cannot silently return.
Related Blueprints
⚠ Normalization Warnings — 12 for review
- CLASSIFICATION TO CONFIRM: 'Prerequisites' classified as a checklist TOOL (gather-before-start items are completable). Alternative: body/prose.
- RESTRUCTURE: 'Primary Prompt' and 'Follow-Up Prompts' (two source H1s) combined into one prompt_pack tool with 8 prompts; 'when' guidance lines are editorial additions, prompt text verbatim (including the doc's run-together formatting in the Primary Prompt).
- CLASSIFICATION TO CONFIRM: 'Test Categories' classified as body/reference (Unit vs Integration definitions the reader consults). Integration example code and Invoke-Pester tag commands folded into the Integration tier definition to avoid losing them — confirm acceptable, or split into a separate example section.
- CLASSIFICATION TO CONFIRM: 'Coverage Guidance' classified as body/reference (a priority model to consult). Alternative: body/prose.
- CLASSIFICATION: 'Security Considerations' kept as body/prose (single narrative with a command list and cautioned-commands block, no subsections); playbook.security_considerations intentionally empty.
- CLASSIFICATION: 'Common Mistakes' kept as a body GROUP (subsection-rich with weak/better code examples) rather than a playbook flat list; playbook.common_mistakes intentionally empty.
- CLASSIFICATION: 'Recommended Test Structure' and 'Running the Tests' classified as body/example (concrete directory layouts and runnable commands the reader consults/copies) rather than tools — they are not fill-in structures.
- CLASSIFICATION: 'CI/CD Example' classified as body/example (a concrete YAML instance to consult/copy). Language set to yaml.
- STATS: overlay.stats.deliverables=3 counts the three protected tools (prerequisites-checklist, pester-prompt-pack, validation-checklist); prompts=8 counts primary + 7 follow-ups.
- PS-004 has no Quick Wins or Roadmap sections; playbook.quick_wins and roadmap intentionally empty.
- Code blocks in Example Input/Output/Analysis were extracted as run-together single paragraphs in the source; reflowed to line breaks for readability but content preserved verbatim — confirm no logic altered.
- EXAMPLE ANALYSIS contains an inline code snippet ([ValidateRange...]) rendered as a code block within the prose example rather than a separate language-tagged section.
SEO Block
- Title tag: Generate Pester Tests for PowerShell Scripts | ABME (51 chars)
- Meta: Generate a safe Pester 5 test suite for any PowerShell script — behavior-focused tests, mocked dependencies, and zero risk to production systems. (145 chars)
- Schema: HowTo · noindex: false
- Related: ps-001, ps-002, ps-003, ps-005, ps-006, ps-007, ps-009, ps-010
- Keywords: pester tests, powershell unit testing, pester 5, mock get-aduser powershell, powershell test automation, pester mock external dependencies, powershell ci/cd testing, characterization tests powershell, pester code coverage, powershell regression testing
