Testing PowerShell Code with Pester

PowerShell Intermediate/Advanced

Chapter 10 · Testing PowerShell Code with Pester

Every function written across both courses has been verified by eye — run it, read the output, decide it looks right. Pester is PowerShell's own built-in testing framework, and it formalizes exactly that instinct into something repeatable: write the expected behavior down once, and let a single command confirm it's still true every time the code changes.

The Structure: Describe, Context, It

# Get-Square.Tests.ps1 Describe "Get-Square" { It "returns the square of a positive number" { Get-Square -n 4 | Should -Be 16 } It "returns 0 for an input of 0" { Get-Square -n 0 | Should -Be 0 } }

Describe groups every test for one thing — usually one function. It is a single test case, named in plain English describing what's actually being checked. Context (not shown above) nests further, grouping related scenarios inside a Describe — "when the input is negative," "when the file doesn't exist" — for tests large enough to benefit from that extra layer.

Should: Making Assertions

AssertionChecks
Should -BeEqual (case-insensitive for strings)
Should -BeExactlyEqual, case-sensitive
Should -BeNullOrEmptyValue is $null or empty
Should -ContainA collection contains a given value
Should -ThrowThe code block raises a terminating error
Should -ExistA file or path actually exists

Should -Be vs. Should -BeExactly: A Familiar Theme, Again

It "matches case-insensitively by default" { "HELLO" | Should -Be "hello" # passes } It "BeExactly requires the same case" { "HELLO" | Should -BeExactly "hello" # fails — case must match exactly }
The central fact this chapter is built on
This is the exact same theme this whole course keeps returning to: Fundamentals 4's -eq was case-insensitive by default, with -ceq as the explicit case-sensitive variant. Chapter 2's -match was case-insensitive by default, with -cmatch as the explicit variant. Should -Be follows the identical pattern — case-insensitive comparison by default, with Should -BeExactly as the deliberate opt-in for anything genuinely case-sensitive. Once you've internalized it once, it applies consistently everywhere in PowerShell.

Testing Error Conditions: Should -Throw

It "throws when given a negative number" { { Get-Square -n -1 } | Should -Throw }

The code under test has to be wrapped in a script block ({ ... }) for Should -Throw — running it directly would throw immediately and fail the test before Should ever gets a chance to check anything.

Mocking: Isolating a Function From Its Real Dependencies

Describe "Get-ConfigValue" { It "reads a value from the config file" { Mock Get-Content { return '{"setting": "value"}' } Get-ConfigValue -Name "setting" | Should -Be "value" # Get-Content never touched a real file — Mock replaced it completely for this test } }

Mock replaces a real cmdlet's behavior for the duration of the test — Get-ConfigValue can be tested without a real config file existing anywhere on disk, and without the test depending on that file's contents staying the same over time. This is what actually makes a test trustworthy: it fails only when the function's own logic is wrong, never because of something unrelated in the surrounding environment.

Setup & Teardown: BeforeAll/BeforeEach, and TestDrive:

Describe "File Cleanup" { BeforeAll { New-Item -Path "TestDrive:\sample.txt" -ItemType File } It "the test file exists" { "TestDrive:\sample.txt" | Should -Exist } }

TestDrive: is a real PSDrive — Fundamentals 2's own provider model at work — that Pester creates automatically for each test run and tears down completely afterward, so file-based tests never leave real files scattered on disk. BeforeAll runs once before every test in its Describe/Context; BeforeEach runs before every single It; AfterAll/AfterEach mirror both for cleanup.

Running Tests: Invoke-Pester

Invoke-Pester -Path .\Get-Square.Tests.ps1 -Output Detailed
The *.Tests.ps1 naming convention isn't optional
Pester's own test discovery specifically looks for files ending in .Tests.ps1 — a file named GetSquareTests.ps1 (missing that literal dot before Tests) won't be picked up at all when Invoke-Pester is run against a folder with no explicit -Path to that one file. It's a real, load-bearing naming convention, not a style preference.
A first practical habit
Write the failing test first — It "returns 0 for an input of 0" { Get-Square -n 0 | Should -Be 0 } before Get-Square even handles that case correctly. Watching it fail, then pass once the code is fixed, is real proof the test is actually checking something, not just quietly passing regardless of what the code does.

Hands-On Exercises

Exercise 1

Explain why "HELLO" | Should -Be "hello" passes while "HELLO" | Should -BeExactly "hello" fails, tying this back to Fundamentals 4's own -eq/-ceq comparison behavior.

📄 View solution
Exercise 2

Explain what Mock Get-Content { return '...' } actually does during a test, and why this matters for writing a test that doesn't depend on a real file existing on disk.

📄 View solution
Exercise 3

Explain why a test file named GetSquareTests.ps1 (without the dot before "Tests") wouldn't be picked up by Invoke-Pester run against a folder with no explicit -Path to that specific file.

📄 View solution

Chapter 10 Quick Reference

  • Describe / Context / It — group tests for a thing, group scenarios within that, and a single named test case
  • Should -Be — case-insensitive equality; Should -BeExactly — case-sensitive, the same pattern as -eq/-ceq and -match/-cmatch
  • Should -Throw — wrap the code in { ... }; testing that it actually raises a terminating error
  • Mock — replaces a real cmdlet's behavior for a test, isolating the function under test from real files/network/state
  • TestDrive: — a real, auto-cleaned PSDrive for file-based tests (Fundamentals 2's provider model at work)
  • BeforeAll/BeforeEach/AfterAll/AfterEach — setup and teardown at different scopes
  • Invoke-Pester — runs tests; requires the *.Tests.ps1 naming convention for automatic discovery
  • Next chapter: Profiles, Customization & Productivity