Loading
0x160Lesson 23 of 24

Testing with Pester, and debugging

Prove your scripts work with Pester tests and mocks, and hunt bugs with Write-Debug, breakpoints and the debugger.

32 min 7-question quiz 2 code exercises
By the end of this lesson you can
  • 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:

  • Describe groups tests for one thing, Context groups tests for one situation.
  • It is one test with a description of the behavior it checks.
  • Should makes an assertion: -Be, -BeExactly, -BeTrue, -Contain, -Match, -BeGreaterThan, -Throw, -HaveCount ...
  • BeforeAll runs setup once - usually dot-sourcing the script under test.

Run them with Invoke-Pester (or the Run Tests button in VS Code).

Feeding.ps1
1function Get-FoodRation {
2  param([Parameter(Mandatory)][ValidateRange(0, 40)][int]$Wingspan)
3  [math]::Max(1, $Wingspan * 3)
4}
Feeding.Tests.ps1
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}
Invoke-Pester output
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: 0

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

  • Mock replaces a command with a fake for the duration of the test. Fake Invoke-RestMethod to return a canned forecast, or Get-Date to make it always Saturday. Should -Invoke then checks how the fake was called.
  • TestDrive: is a throwaway temporary folder, wiped after each Describe block - perfect for testing scripts that read and write files.
Weather.Tests.ps1
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.

Hatchery.Tests.ps1 (1 of 1)Flags found 0/0

Click every part that looks suspicious. There are 6.

BeforeAll { . $PSScriptRoot\Hatchery.ps1 } Describe 'Hatchery' { It 'works' { $egg = New-Egg -Species Wisp Add-Warmth -Egg $egg -Degrees 12 } It 'fetches the egg registry' { $eggs = Invoke-RestMethod 'https://api.emberfall.example/eggs' $eggs.Count | Should -BeGreaterThan 0 } $script:sharedEgg = New-Egg -Species Wyrm It 'only schedules hatching on weekdays' { (Get-Date).DayOfWeek | Should -Not -Be Saturday } It 'cleans old records' { Remove-OldRecords -Path 'C:\Sanctuary\Records' Get-ChildItem C:\Sanctuary\Records | Should -HaveCount 0 } }

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:

  1. Write-Debug messages 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.
  2. Set-PSDebug -Trace 1 prints every line as it runs. Noisy, but great for “how did it even get here?”. Turn it off with Set-PSDebug -Off.
  3. Breakpoints pause the script so you can look around.
write-debug.ps1
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 { "$_" }
Output
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:

KeyDoes
sStep into the next line (including into functions)
vStep over - run the next line without going into functions
oStep out of the current function
cContinue to the next breakpoint
lList the code around where you are
kShow the call stack - who called whom
qQuit 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.

breakpoints.ps1
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-Error

Key takeaways

  • Pester tests live in *.Tests.ps1: BeforeAll dot-sources the code, Describe/It organise, Should asserts.

  • It -ForEach runs one test for many cases; Should -Throw takes a script block.

  • Mock replaces 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-Debugger and Get-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.

Exercise 1

A test runner in miniature

+25 XP

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
script.ps1
Loading editor…

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.

Exercise 2

Debug the keeper rota

+25 XP

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
script.ps1
Loading editor…

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…

Did you like the lesson? 😆👍
Consider a donation to support our work: