Learn PowerShell - Testing with Pester
Episode 24 of 31

Learn PowerShell - Testing with Pester

Tests aren't a bonus but a safety net that proves scripts work before they touch production. This episode covers Pester: the Describe, Context, and It structure, assertions with Should, mocking, code coverage, and running tests in CI/CD pipelines.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 23 you made scripts that can explain themselves — logging, debugging, and profiling. But one leap of trust remains: a script that runs smoothly on your laptop isn't necessarily correct in production. How do you prove it?

That's the role of tests. A test is an automated quality check: not "I think this script works", but "I have 40 tests proving it, and they're all green". Like before hitting the highway, it's not enough to say "I can drive" — you pass the driving test. This episode gives your scripts that driving test using Pester, the de facto testing framework for PowerShell.

Getting to Know Pester

Pester is the official PowerShell community testing framework. Its structure is simple and consistent: each test is written as a behavior description in a Describe block containing It blocks as specific statements. A well-written test serves double duty: specification and proof at once.

Important: Windows PowerShell 5.1 ships with an old Pester version (3.4). For modern features, install the latest version 5:

Installing Pester
Install-Module -Name Pester -Scope CurrentUser -Force
Import-Module Pester -MinimumVersion 5.0

Install-Module fetches Pester from the PowerShell Gallery. Add -Scope CurrentUser so it's installed just for your account — without needing admin.

Anatomy of a Test File

A Pester test is a .Tests.ps1 script placed alongside the function under test. The basic framework:

Your first test framework
Describe "Fungsi Get-ServerUptime" {
    Context "Saat server aktif" {
        It "Mengembalikan durasi uptime" {
            $result = Get-ServerUptime -ServerName "WEB-01"
            $result | Should -Not -BeNullOrEmpty
        }
    }
}

Three blocks you must master:

  • Describe — the test suite: a large group, usually one per function or module.
  • Context — the condition: grouping tests by a particular state ("when the server is active", "when input is empty").
  • Itone test case: a single specific statement that can pass or fail.

Read It block names as a full sentence: "when the server is active, it returns the uptime duration". Tests that read like sentences are living documentation — others read them like a spec, not code.

Assertions with Should

The heart of every test is the assertion: the statement "I believe the result is this". In Pester, assertions are written with Should:

OperatorChecksExample
-BeValue equalityShould -Be 42
-BeExactlyExact case-sensitive equalityShould -BeExactly "OFFLINE"
-BeGreaterThanGreater thanShould -BeGreaterThan 0
-ContainCollection membershipShould -Contain "WEB-01"
-ThrowAn error is thrownShould -Throw
-NotNegationShould -Not -BeNullOrEmpty

Example assertions on a unit-conversion function:

Complete assertions
Describe "Fungsi ConvertTo-Kilobyte" {
    It "Mengubah 2048 byte menjadi 2 kilobyte" {
        ConvertTo-Kilobyte -Bytes 2048 | Should -Be 2
    }
    It "Menolak nilai negatif" {
        { ConvertTo-Kilobyte -Bytes -1 } | Should -Throw
    }
}

Note the second line: the script block inside the curly braces is passed as a delegation to Should -Throw — Pester runs it and makes sure it throws an error. This pattern is essential for testing bad behavior, not just the happy path.

Tip

Hold the discipline: one It, one thing tested. A test checking three things at once misleads — when it fails, you don't know which part broke. A test that fails at the first assertion never reaches the next one, so splitting assertions means sharper diagnostic information.

Unit Tests and Integration Tests

Tests fall into two major types:

  • Unit tests — test one function in isolation. Fast, deterministic, no real system contact. These are the majority of your tests.
  • Integration tests — test collaboration between components: function + database, function + network service, function + Active Directory. Slow but prove that individually correct parts are also correct when assembled.

Workshop analogy: unit tests are inspecting the engine, brakes, and suspension one by one on the workbench; integration tests are driving the car on the road. Both are needed — a vehicle whose components all pass inspection can still break when assembled.

Mocking to Isolate Dependencies

The unit test problem: how do you test a function that calls an external cmdlet — for example Copy-Item copying real files? Calling it in every test is slow, non-deterministic, and dangerous. The solution is mocking: replacing the real cmdlet's behavior with a fake one inside the test.

Mocking external dependencies
Describe "Skrip Start-Backup" {
    It "Berjalan tanpa menyentuh sistem file nyata" {
        Mock Copy-Item { param($Source, $Destination) "Menyalin $Source" }
        Mock Remove-Item { }
 
        { Start-Backup -Source "C:\Data" } | Should -Not -Throw
    }
}

With Mock, tests become fast, runnable repeatedly without side effects, and independent of machine conditions. Real behavior is still tested — by separate integration tests run rarely, for example only in a nightly pipeline.

Test Coverage

Coverage answers the question: of all the code lines, what percentage is actually executed by tests? Pester measures it via the -CodeCoverage parameter:

Measuring code coverage
Invoke-Pester -Path ".\tests" -CodeCoverage ".\functions\*.ps1"

The result shows the percentage of tested lines and which files remain untouched. Coverage is radar: untested areas are clearly visible, so you know where the next test is most valuable.

Important

High coverage isn't a guarantee of correctness. Tests can touch 100 percent of code lines without ever checking the right values — only making sure nothing errors. Treat coverage as a blind-spot finder, not a number target. Focus on meaningful tests: boundary conditions, invalid input, and behaviors that have already failed before.

Running Tests: Invoke-Pester

To run all tests, use Invoke-Pester:

Running tests
Invoke-Pester -Path ".\tests"
Invoke-Pester -Path ".\tests" -Output Detailed
Invoke-Pester -Path ".\tests" -CI
  • -Output Detailed shows each It's name with its result — most useful when hunting failures.
  • -CI enables pipeline-friendly mode: concise output, no colors and no special characters, suitable for CI logs.

To report results to other tools, export in the NUnit XML format understood by almost every CI system:

Exporting test results
$result = Invoke-Pester -Path ".\tests" -PassThru
$result | ConvertTo-NUnitXml -Path ".\TestResults.xml" -ErrorAction SilentlyContinue

Integrating Tests into CI/CD

Tests only run manually on a laptop are half-dead tests — they never protect anything. The end goal: tests run automatically on every commit, and the pipeline fails if any test is red. In GitHub Actions, one step is enough:

Test step in GitHub Actions
- name: Jalankan test Pester
  shell: pwsh
  run: Invoke-Pester -Path "./tests" -CI

With this pattern, every code change faces a gate: before a change can be merged into the main branch, all tests must pass. This is the safety net that makes teams brave enough to refactor — because you know whatever breaks will shout, not whisper.

Conclusion

In this episode 24 you've built a testing foundation: installing Pester with Install-Module; test structure with Describe, Context, and It reading like specifications; assertions with Should and the -Be, -Contain, -Throw, -Not operators; the difference between unit and integration tests; mocking external dependencies with Mock; measuring code coverage with -CodeCoverage; running and exporting results with Invoke-Pester and ConvertTo-NUnitXml; and integration into CI/CD pipelines.

Key takeaways:

  • Tests are runnable specifications, not just proof.
  • One It, one thing tested; test names read like sentences.
  • Mock dependencies so unit tests are fast and deterministic.
  • Coverage finds blind spots; it isn't a guarantee of correctness.
  • Tests that don't run in CI are tests that protect nothing.

Your scripts can now prove themselves. But tests and scripts themselves can still be deleted, overwritten, or collide between team members. In the next episode, episode 25, we answer that: PowerShell & Git — putting scripts under version control, collaboration through branching, test automation in pipelines, and publishing modules to the PowerShell Gallery. See you there!

Learn PowerShell - Testing with Pester | Learn PowerShell