Testing with Pester, and debugging
Prove your scripts work with Pester tests and mocks, and hunt bugs with Write-Debug, breakpoints and the debugger.
- Write Pester tests with Describe, It and Should, including data-driven cases
- Isolate code from the outside world with Mock and TestDrive:
- Find bugs with Write-Debug, breakpoints, the interactive debugger and Set-PSDebug
Your feeding script works today. Will it still work after Grace adds a feature next month? Tests answer that in seconds: small scripts that run your code with known inputs and check the results. PowerShell’s testing framework is Pester - it ships with Windows, but get the modern version with Install-Module Pester -Scope CurrentUser -Force (version 5 or later).
A test file is named Something.Tests.ps1 and sits next to the code it tests. Its vocabulary reads almost like English:
Describegroups tests for one thing,Contextgroups tests for one situation.Itis one test with a description of the behavior it checks.Shouldmakes an assertion:-Be,-BeExactly,-BeTrue,-Contain,-Match,-BeGreaterThan,-Throw,-HaveCount...BeforeAllruns setup once - usually dot-sourcing the script under test.
Run them with Invoke-Pester (or the Run Tests button in VS Code).
1function Get-FoodRation {
2 param([Parameter(Mandatory)][ValidateRange(0, 40)][int]$Wingspan)
3 [math]::Max(1, $Wingspan * 3)
4}1BeforeAll {
2 . $PSCommandPath.Replace('.Tests.ps1', '.ps1')
3}
4
5Describe 'Get-FoodRation' {
6 It 'gives 3 kg per metre of wingspan' {
7 Get-FoodRation -Wingspan 4 | Should -Be 12
8 }
9
10 It 'never gives less than 1 kg, even to hatchlings' {
11 Get-FoodRation -Wingspan 0 | Should -Be 1
12 }
13
14 It 'rejects impossible wingspans' {
15 { Get-FoodRation -Wingspan 99 } | Should -Throw
16 }
17
18 It 'gives <Expected> kg for wingspan <Wingspan>' -ForEach @(
19 @{ Wingspan = 1; Expected = 3 }
20 @{ Wingspan = 12; Expected = 36 }
21 @{ Wingspan = 40; Expected = 120 }
22 ) {
23 Get-FoodRation -Wingspan $Wingspan | Should -Be $Expected
24 }
25}1Starting discovery in 1 files.
2Discovery found 6 tests in 45ms.
3Running tests.
4[+] C:\Sanctuary\Feeding.Tests.ps1 212ms (98ms|79ms)
5Tests completed in 220ms
6Tests Passed: 6, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0Mocks and TestDrive: tests that don’t touch the real world
Good tests are fast, repeatable and safe. A test that calls the real weather API fails when the wifi does; a test that deletes real files is a disaster waiting to happen. Pester gives you two escape hatches:
Mockreplaces a command with a fake for the duration of the test. FakeInvoke-RestMethodto return a canned forecast, orGet-Dateto make it always Saturday.Should -Invokethen checks how the fake was called.TestDrive:is a throwaway temporary folder, wiped after eachDescribeblock - perfect for testing scripts that read and write files.
1BeforeAll {
2 function Get-FlyingWeather {
3 $forecast = Invoke-RestMethod 'https://weather.emberfall.example/today'
4 if ($forecast.windKph -gt 60) { 'grounded' } else { 'clear to fly' }
5 }
6}
7
8Describe 'Get-FlyingWeather' {
9 It 'grounds dragons in a gale' {
10 Mock Invoke-RestMethod { [pscustomobject]@{ windKph = 85 } }
11 Get-FlyingWeather | Should -Be 'grounded'
12 Should -Invoke Invoke-RestMethod -Times 1 -Exactly
13 }
14
15 It 'writes the log where we asked' {
16 $log = Join-Path TestDrive: 'flights.log'
17 'Skyla 14:00' | Set-Content $log
18 Get-Content $log | Should -HaveCount 1
19 }
20}Try it
Spot the flaky tests
This test file passes today, but it’s going to cause trouble. Click every line that makes the tests unreliable, slow, dangerous or useless.
Click every part that looks suspicious. There are 6.
Debugging: finding out what really happens
A test tells you that something is wrong. Debugging tells you why. Start gentle, then bring out the heavy tools:
Write-Debugmessages are silent until you ask for them: run an advanced function with-Debug, or set$DebugPreference = 'Continue'for the whole script. Leave them in - they cost nothing when switched off.Set-PSDebug -Trace 1prints every line as it runs. Noisy, but great for “how did it even get here?”. Turn it off withSet-PSDebug -Off.- Breakpoints pause the script so you can look around.
1function Get-Ration([int]$Wingspan) {
2 Write-Debug "wingspan is $Wingspan"
3 $Wingspan * 3
4}
5Get-Ration 4
6$DebugPreference = 'Continue'
7Get-Ration 5 5>&1 | ForEach-Object { "$_" }12 wingspan is 5 15
Set-PSBreakpoint stops a script at a line, when a variable changes, or when a command is called. When it stops, you get a [DBG] prompt where you can inspect variables (just type $hours) and step:
| Key | Does |
|---|---|
s | Step into the next line (including into functions) |
v | Step over - run the next line without going into functions |
o | Step out of the current function |
c | Continue to the next breakpoint |
l | List the code around where you are |
k | Show the call stack - who called whom |
q | Quit the debugger and stop the script |
In VS Code with the PowerShell extension it’s even easier: click left of a line number to set a breakpoint, press F5, and hover over variables. Put Wait-Debugger in a script to pause there on purpose - handy inside jobs and remote sessions.
1# Stop at line 12 of the rota script
2Set-PSBreakpoint -Script .\rota.ps1 -Line 12
3
4# Stop whenever $total is written to
5Set-PSBreakpoint -Script .\rota.ps1 -Variable total -Mode Write
6
7# Stop every time Remove-Item is called
8Set-PSBreakpoint -Command Remove-Item
9
10.\rota.ps1
11# [DBG]: PS C:\Sanctuary>> $hours
12# [DBG]: PS C:\Sanctuary>> v
13
14Get-PSBreakpoint | Remove-PSBreakpoint
15
16# After an error: the full story, including the inner exception and stack
17Get-ErrorKey takeaways
Pester tests live in
*.Tests.ps1:BeforeAlldot-sources the code,Describe/Itorganise,Shouldasserts.It -ForEachruns one test for many cases;Should -Throwtakes a script block.Mockreplaces commands like Invoke-RestMethod or Get-Date;TestDrive:is a safe scratch folder.Debug with
Write-Debug,Set-PSDebug -Trace 1,Set-PSBreakpoint(line, variable or command),Wait-DebuggerandGet-Error.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: write PowerShell scripts
Write a script in the editor and run it for real against sample input, which is piped into your script as $input. Scripts run on PowerShell 6.2 through Try It Online (tio.run), a free public service, so these exercises avoid PowerShell 7-only syntax; your script and test input are sent there.
A test runner in miniature
Pester isn’t available here, so build its heart yourself. Get-FoodRation should give 3 kg per metre of wingspan, but never less than 1 kg - and it has a bug. Don’t fix it: write tests that catch it.
Each input line is a test case: wingspan expected. Run the function and print [PASS] wingspan 4 = 12 kg or [FAIL] wingspan 0 = 1 kg: got 0. Finish with 3 passed, 1 failed.
The starter’s “test” never compares anything - a test that can’t fail is worse than no test.
- Catches the hatchling bug
- All green
Lessons teach PowerShell 7; your script runs on PowerShell 6.2 via Try It Online (tio.run), a free public service, so stick to syntax that works there. Test input is piped into your script as $input. Your script and test input are sent to that service.
Debug the keeper rota
Each input line is a keeper and the lengths of their shifts in hours, like Ada 9 9 9 9 8. The script should print Ada: 44h OVERTIME (more than 40 hours) or Grace: 19h, then total: 69 h for everyone.
The starter has two bugs. Hunt them like a debugger would: add Write-Debug lines (with $DebugPreference = 'Continue'), or check $hours.GetType() - then fix them. Remove your debug output before you submit.
- Three keepers
- Right on the line
Lessons teach PowerShell 7; your script runs on PowerShell 6.2 via Try It Online (tio.run), a free public service, so stick to syntax that works there. Test input is piped into your script as $input. Your script and test input are sent to that service.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…