Skip to content
6 min read

Running Tests with Invoke-Pester and Reading the Output

  • PowerShell
  • Pester
  • Invoke-Pester
  • Output
  • Beginners

A green checkmark is nice, but the real skill is reading a failure and knowing exactly what broke without opening the source file. In Part 3 you wrote a test and ran it. Here we slow down on the run itself—from pointing Invoke-Pester at a folder to decoding a failure and understanding the exit code that CI depends on.

What you'll learn

  • How to run tests on a single file and on a whole folder
  • How to read the pass/fail/skipped summary line
  • How to decode a failure message (Expected vs But was)
  • How -Output Detailed changes what you see
  • What exit codes are and why CI relies on them

Running tests on a file or a folder

Invoke-Pester is the command that finds and runs your tests. The simplest form points it at a single test file:

Invoke-Pester -Path ./Get-Square.Tests.ps1

More often you point it at a folder, and Pester discovers every *.Tests.ps1 file underneath it, recursively:

# Runs every *.Tests.ps1 file under the Tests directory and its subfolders
Invoke-Pester -Path ./Tests

That discovery-by-convention is why the .Tests.ps1 suffix matters (more on file layout in Part 12): give Invoke-Pester a folder and it runs the lot.

Reading the summary line

Whatever you run, Pester ends with a one-line summary. A healthy run looks like this:

Tests completed in 142ms
Tests Passed: 2, Failed: 0, Skipped: 0 NotRun: 0

Read that line first, every time. Failed: 0 is the number that decides whether you can relax. Passed tells you how much actually ran (a suite that reports Passed: 0 usually means nothing was discovered, often a naming or path problem, not a success). Skipped and NotRun matter once you start filtering tests by tags in Part 16.

Anatomy of a failure

Let us make one fail on purpose, the most useful thing you can do while learning. Save this file as Get-Square.Tests.ps1 with a deliberate bug in the function:

function Get-Square {
    param([int]$Number)
    $Number + $Number   # BUG: should be *, not +
}

Describe 'Get-Square' {
    It 'returns 25 for an input of 5' {
        Get-Square -Number 5 | Should -Be 25
    }
}

Run it with Invoke-Pester -Path ./Get-Square.Tests.ps1 and Pester prints a failure block. Annotated, it reads like this:

Describing Get-Square
  [-] returns 25 for an input of 5 8ms (5ms|3ms)
   Expected 25, but got 10.
   at Get-Square -Number 5 | Should -Be 25, /Get-Square.Tests.ps1:8
   at <ScriptBlock>, /Get-Square.Tests.ps1:8

Tests completed in 142ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0

Three parts to notice:

  • [-] and the test name tell you which behavior broke: "returns 25 for an input of 5." This is why good It names pay off.
  • Expected 25, but got 10. is the heart of it. Expected is what your Should -Be claimed; but got (sometimes shown as "but was") is what the code actually produced. The gap between them is the bug: 5 + 5 is 10, not 25.
  • The at ... :8 lines point to the exact file and line number, so you go straight to the problem.

You diagnosed a bug without reading the function body, purely from the failure. That is the skill.

Turning up the detail with -Output

By default Pester keeps the console quiet, often showing only failures and the summary. While learning (or hunting a stubborn bug) you usually want to see every test, passing and failing alike. Use -Output Detailed:

Invoke-Pester -Path ./Get-Square.Tests.ps1 -Output Detailed

Now you see each Describe, each It, and a green [+] for every pass alongside the red [-] failures:

Describing Get-Square
  [+] returns 0 for an input of 0 4ms
  [-] returns 25 for an input of 5 8ms
   Expected 25, but got 10.

The verbosity levels are None, Normal, Detailed, and Diagnostic. Detailed is the sweet spot for day-to-day work; Diagnostic is loud and mostly for debugging Pester itself. (Part 23 sets this through a configuration object instead of a switch.)

Exit codes: why CI cares

Every command leaves behind an exit code, a number that other tools read to decide "did that succeed?" Zero means success; non-zero means failure. After an Invoke-Pester run you can inspect it:

Invoke-Pester -Path ./Tests
$LASTEXITCODE   # 0 if all passed, non-zero if any test failed

This is the single most important thing CI systems use. A pipeline does not read your console output, it checks the exit code: if Invoke-Pester exits non-zero, the build goes red and the bad change is blocked. To make sure Pester actually sets that code (it does not always, depending on how it is invoked), pass -CI in automation:

# In CI: ensure a failing suite produces a non-zero exit code
Invoke-Pester -Path ./Tests -CI

Part 24 wires this into real GitHub Actions and Azure Pipelines workflows. For now, just remember: green console, exit code 0, happy CI.

Try it yourself

  1. Save the buggy Get-Square.Tests.ps1 above (with + instead of *).
  2. Run Invoke-Pester -Path ./Get-Square.Tests.ps1 -Output Detailed.
  3. Read the Expected vs but got line and the line number. Resist the urge to open the function, can you name the bug from the message alone?
  4. Check the result of $LASTEXITCODE and confirm it is non-zero.
  5. Fix the function (* instead of +), rerun, and confirm green with $LASTEXITCODE back to 0.

Common mistakes

Running the .Tests.ps1 file directly. Executing ./Get-Square.Tests.ps1 (or dot-sourcing it) does not run your tests the way Pester intends, in v5 the file is processed in two phases and needs Invoke-Pester to drive it. Always run tests through Invoke-Pester -Path.

Ignoring the summary line. It is easy to glance at a wall of output and miss that Failed: 1. Read the summary first, every time; the Failed count is the verdict.

Not knowing the exit code drives CI. A pipeline does not read your eyes, it reads $LASTEXITCODE. If you forget -CI (or the equivalent config) in automation, a red suite can still report "build succeeded," which is worse than no tests at all.

Recap

Invoke-Pester -Path runs a single file or discovers every *.Tests.ps1 under a folder. Read the summary line first, the Failed count is your verdict, then decode failures from the Expected-vs-but-got message and the file:line pointer, no need to open the source. -Output Detailed shows passes and failures together while you learn, and the exit code ($LASTEXITCODE, made reliable with -CI) is what turns a red test into a failed build.

Next up: Part 5 — The Should Command and Core Operators, where we leave -Be behind for a moment and meet the handful of assertion operators that cover most of the tests you will ever write.