Chapter 18Lesson 01~215 minutes

Pester Fundamentals: Describe, Context, It, and Assertions

Learn current Pester 6 fundamentals by turning small PowerShell behaviors into readable tests with Describe, Context, It, setup/teardown, and modern Should-* assertions.

Pester 6AssertionsTest designFeedback

Learning objectives

  • Distinguish unit, integration, acceptance, and smoke testing for automation.
  • Use the verified Pester 6 testing model rather than obsolete invocation syntax.
  • Organize tests with Describe, Context, It, and setup/teardown blocks.
  • Use current Should-* assertions and recognize classic assertion syntax.
  • Interpret test failures as behavior feedback instead of opaque errors.
  • Build, run, repair, and clean up a local disposable Pester suite.

1. Tests are executable expectations, not ceremonial files

Automation changes systems quickly, so a small defect can repeat across many hosts, repositories, or deployments. A test is executable evidence that a specific behavior still matches an expectation. Good tests shorten the feedback loop: instead of discovering a regression during a release, you detect it while changing the code.

Testing does not mean every check has the same scope. A useful testing pyramid puts many fast, isolated tests near the bottom and fewer expensive end-to-end checks near the top.

Tests are executable expectations, not ceremonial files
flowchart TD;
    U["Many fast unit tests"] --> I["Fewer integration tests"];
    I --> A["Small number of acceptance tests"];
    A --> S["Focused smoke tests after deployment"];
  
Test level Question Typical PowerShell example
Unit Does one small behavior work in isolation? Transform a config object without touching disk or network.
Integration Do real components work together? Write to a temporary filesystem or call a local service.
Acceptance Does the workflow satisfy the user/operational contract? Run a deployment workflow against a disposable lab.
Smoke Is the deployed system basically alive? Check a health endpoint and one critical dependency.

2. The current baseline is Pester 6

Pester is PowerShell's widely used testing framework. For this course, the verified current stable baseline is Pester 6.0.0. Pester 6 still runs the familiar Describe, Context, and It blocks from Pester 5, but it also adds a newer family of Should-* assertion commands and removes several long-deprecated invocation patterns.

Version rule for this chapter: examples target Pester 6.0.0. When you inherit an older suite, expect classic Pester 5 assertion syntax such as $value | Should -Be 3. It remains supported in Pester 6, but new tests here prefer $value | Should-Be 3.
$pester = Get-Module -ListAvailable Pester |
    Sort-Object Version -Descending |
    Select-Object -First 1

if ($pester) {
    $pester | Select-Object Name,Version,Path
} else {
    'Pester is not installed in a discoverable module path.'
}

# If Chapter 15's PSResourceGet tooling is available and installation is needed:
# Install-PSResource -Name Pester -Version 6.0.0 -Scope CurrentUser

Do not silently upgrade a shared build agent during a test run. In production CI, pin the tested Pester version in the runner image, bootstrap step, or repository dependency policy.

3. Start with a pure transformation

A pure function computes output from input without modifying external state. It does not write a file, contact an API, or depend on the current clock. Pure functions are excellent first test subjects because failures point directly at logic rather than environmental setup.

function ConvertTo-ReleaseRecord {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $Name,

        [Parameter(Mandatory)]
        [ValidateRange(1,100)]
        [int] $Replicas,

        [string] $Environment = 'dev'
    )

    [pscustomobject]@{
        Name        = $Name.Trim().ToLowerInvariant()
        Replicas    = $Replicas
        Environment = $Environment.Trim().ToLowerInvariant()
    }
}

ConvertTo-ReleaseRecord -Name ' API ' -Replicas 3 -Environment ' STAGE ' 

The output contract is stable and observable: the function returns one object with three properties. That makes the desired behavior straightforward to express as tests.

4. Describe groups behavior; Context narrows a scenario; It states one expectation

Pester discovers files named *.Tests.ps1. A common pattern is to load the code under test in BeforeAll, then organize expectations into readable blocks.

# tests/ReleaseTools.Tests.ps1
BeforeAll {
    . "$PSScriptRoot/../src/ReleaseTools.ps1"
}

Describe 'ConvertTo-ReleaseRecord' {
    Context 'when input contains mixed case and whitespace' {
        It 'normalizes the application name' {
            $record = ConvertTo-ReleaseRecord -Name ' API ' -Replicas 3
            $record.Name | Should-Be 'api'
        }

        It 'preserves the validated replica count' {
            $record = ConvertTo-ReleaseRecord -Name 'api' -Replicas 3
            $record.Replicas | Should-Be 3
        }
    }
}

Describe names the behavior under test. Context explains the condition or scenario. It should read like a requirement. A reader should understand what broke from the test name before reading the implementation.

5. Assertions convert observations into pass/fail signals

An assertion compares an actual observation with an expected condition. Pester 6's new Should-* commands are type-aware and provide clearer diagnostics for many shapes of data. Use a collection assertion when collection shape matters; use -Actual when the PowerShell pipeline would otherwise unwrap or reshape a value.

Describe 'Pester 6 assertion examples' {
    It 'compares scalar values' {
        42 | Should-Be 42
    }

    It 'checks a boolean result' {
        (3 -gt 1) | Should-BeTrue
    }

    It 'checks a collection as a collection' {
        1,2,3 | Should-BeCollection @(1,2,3)
    }

    It 'checks an exception boundary' {
        { throw 'simulated failure' } | Should-Throw
    }
}

Classic assertions such as 42 | Should -Be 42 still work in Pester 6. New code in this chapter uses the newer command form so learners see the current recommended surface.

6. A failing test is useful only when it explains the contract

A red test is not the enemy. It is feedback. An intentionally failing test is useful during training because it shows how Pester reports the expected value, actual value, test name, and source location.

Describe 'failure message demonstration' {
    It 'expects the normalized environment to be prod' {
        $record = ConvertTo-ReleaseRecord -Name 'api' -Replicas 2 -Environment 'stage'

        # Intentionally wrong for the first run of this exercise:
        $record.Environment | Should-Be 'prod' -Because 'this test demonstrates a readable failure'
    }
}

# After observing the failure, repair the expectation to 'stage'
# or change the input to 'prod' depending on the intended requirement.
Training rule: intentionally failing tests belong in a controlled exercise, not committed as a permanently failing main-branch suite. Once you understand the diagnostic, restore the suite to green.

7. Setup and teardown create controlled test state

Pester provides BeforeAll, BeforeEach, AfterEach, and AfterAll. Use them for test state that genuinely belongs to the surrounding block. Avoid hiding the important arrange/act details of every test inside distant setup code.

Describe 'temporary in-memory state' {
    BeforeEach {
        $script:items = [System.Collections.Generic.List[string]]::new()
    }

    AfterEach {
        $script:items.Clear()
    }

    It 'starts each test with an empty list' {
        $script:items.Count | Should-Be 0
        $script:items.Add('api')
        $script:items.Count | Should-Be 1
    }

    It 'does not inherit the previous test mutation' {
        $script:items.Count | Should-Be 0
    }
}

Tests should be independently repeatable. If one test needs another test to run first, the suite is encoding accidental order rather than a reliable behavior contract.

8. Keep each test readable: arrange, act, assert

A useful mental pattern is Arrange → Act → Assert. Arrange creates the input and dependencies. Act executes the behavior once. Assert checks the meaningful result. You do not need literal comments for every test, but the structure should be visible.

It 'lowercases the environment name' {
    # Arrange
    $input = @{ Name='worker'; Replicas=1; Environment='PROD' }

    # Act
    $actual = ConvertTo-ReleaseRecord @input

    # Assert
    $actual.Environment | Should-Be 'prod'
}

Prefer one coherent behavior per It. Multiple assertions are fine when they describe one output contract, but a test that validates unrelated concerns becomes harder to diagnose.

9. Lab — create and run a local Pester 6 suite

This lab uses a disposable directory. It creates a source file and test file, runs the suite, demonstrates one intentional failure, then repairs it. No production system is touched.

$lab = Join-Path ([IO.Path]::GetTempPath()) "ps-academy-pester-$PID"
$src = Join-Path $lab 'src'
$tests = Join-Path $lab 'tests'
New-Item -ItemType Directory -Path $src,$tests -Force | Out-Null

@'
function ConvertTo-ReleaseRecord {
    param([string]$Name,[int]$Replicas,[string]$Environment='dev')
    [pscustomobject]@{
        Name=$Name.Trim().ToLowerInvariant()
        Replicas=$Replicas
        Environment=$Environment.Trim().ToLowerInvariant()
    }
}
'@ | Set-Content -LiteralPath (Join-Path $src 'ReleaseTools.ps1')

@'
BeforeAll { . "$PSScriptRoot/../src/ReleaseTools.ps1" }
Describe 'ConvertTo-ReleaseRecord' {
    It 'normalizes the name' {
        (ConvertTo-ReleaseRecord -Name ' API ' -Replicas 2).Name | Should-Be 'api'
    }
    It 'demonstrates a failure before repair' {
        (ConvertTo-ReleaseRecord -Name 'api' -Replicas 2).Replicas | Should-Be 99
    }
}
'@ | Set-Content -LiteralPath (Join-Path $tests 'ReleaseTools.Tests.ps1')

Invoke-Pester -Path $tests -Output Detailed

# Repair 99 -> 2, rerun, then clean up the lab when finished.
# Remove-Item -LiteralPath $lab -Recurse -Force

Expected observation: the first test passes and the second test fails with an expected-versus-actual message. After repairing the expected value, both tests should pass.

10. Common first-test mistakes

Mistake Why it hurts Better pattern
Test a production API first Network/authentication failures obscure basic test mechanics. Start with pure transformation logic.
Use vague names such as “works” Failure output does not explain the contract. Name the behavior and condition.
Share mutable state across tests Order-dependent, flaky suites appear. Recreate state in BeforeEach or the test itself.
Assert formatting instead of data Display changes break tests without behavior changes. Assert object properties and contracts.
Copy Pester 4 invocation syntax Deprecated/removed parameters fail on current Pester. Use Pester 6 simple or configuration interfaces.

11. Why this matters in DevOps

A PowerShell script can become part of provisioning, release orchestration, incident response, or policy enforcement. Tests make assumptions explicit before those assumptions reach a fleet. They also provide a stable safety net for refactoring: when the internal implementation changes but the public behavior stays the same, the tests should remain green.

12. Verification checklist

  • You can distinguish unit, integration, acceptance, and smoke tests.
  • You know this chapter targets Pester 6.0.0, not old Pester 4 examples.
  • You can structure tests with Describe, Context, It, and setup/teardown blocks.
  • You can use Pester 6 Should-* assertions and recognize classic Should -Be syntax.
  • You can read a failure as an executable statement of expected behavior.
  • You can run a disposable local test suite and return it to a passing state.

13. Knowledge check

Question 1. Why are pure functions good first unit-test targets?

Question 2. What file naming pattern does Pester discover by default?

Question 3. What is the role of an It block?

Question 4. What assertion style is preferred for new tests in Pester 6?

Question 5. Should an intentionally failing training test stay permanently in the main suite?

14. Summary and next bridge

Pester turns behavioral expectations into executable feedback. The lowest-friction path starts with pure logic, readable block names, current Pester 6 assertions, and independent test state. The next lesson moves closer to real automation: files, APIs, clocks, and other side effects—without allowing unit tests to modify production resources.

15. Authoritative references

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.