Anatomy of a Pester Test

A Guide to the Pester DSL

Tommy Becker
- Tue Oct 31 2023

So you’ve decided to start testing your PowerShell code with Pester. That’s a fantastic step towards writing more reliable automation! When you first look at a Pester test file (.Tests.ps1), you’ll see a structure that looks like PowerShell but has its own special vocabulary. This is Pester’s Domain-Specific Language (DSL).

Understanding this DSL is the key to unlocking Pester’s power. Let’s break down the anatomy of a Pester test, block by block.

Describe: The Outermost Container

The Describe block is the top-level container for your tests. Its purpose is to group all the tests related to a single function or script. The name you give the Describe block should be the name of the function you are testing.

Describe 'Get-FormattedDate' {
    # All tests for Get-FormattedDate go here
}

Inside a Describe block, you can use one or more Context blocks. Context is used to group a set of tests for a specific scenario or condition. This helps organize your tests and make them more readable.

For example, you might have one context for when your function receives valid input and another for when it receives invalid input.

Describe 'Get-FormattedDate' {
    Context 'When given a valid date' {
        # Tests for the happy path go here
    }

    Context 'When given an invalid date' {
        # Tests for error handling go here
    }
}

It: The Individual Test Case

The It block is the heart of your test. It represents a single, specific test case that verifies one piece of behavior. The name of the It block should be a human-readable sentence that describes what the function should do.

An It block contains the code to run your function and an assertion to check the result.

It 'Should return the date in YYYY-MM-DD format' {
    $date = Get-Date '2023-10-31'
    $result = Get-FormattedDate -Date $date
    $result | Should -Be '2023-10-31'
}

Setup and Teardown Blocks

Pester provides special blocks to run code before and after your tests. This is perfect for setting up prerequisites (like creating temporary files or mocking commands) and cleaning up afterward.

  • BeforeAll / AfterAll: These run once per Describe or Context block. Use them for setup/teardown that is expensive and can be shared across all tests in that block.
  • BeforeEach / AfterEach: These run before and after every single It block. Use them to ensure each test starts from a clean, isolated state.

Should: The Assertion

The Should command is Pester’s assertion engine. It’s how you check if the actual output of your code matches the expected output. It uses a natural language syntax that is easy to read.

  • $result | Should -Be 'ExpectedValue' (for simple values)
  • $result | Should -BeNullOrEmpty (to check for null or empty output)
  • { My-Function } | Should -Throw 'Error message' (to check for errors)

Pester has a rich set of assertion operators for almost any scenario you can imagine.

Putting It All Together

Here’s a complete example showing how all these pieces fit together to test a simple function.

Describe 'Get-FileUpperCase' {
    # Runs once before any tests in this block
    BeforeAll {
        # Create a temporary directory for test files
        $script:tempDir = New-Item -Path (Join-Path $env:TEMP ([System.Guid]::NewGuid())) -ItemType Directory
    }

    # Runs once after all tests in this block are complete
    AfterAll {
        # Clean up the temporary directory
        Remove-Item -Path $script:tempDir -Recurse -Force
    }

    Context 'When the file exists' {
        # Runs before each 'It' block in this Context
        BeforeEach {
            $script:testFile = Join-Path $script:tempDir.FullName -ChildPath 'test.txt'
            Set-Content -Path $script:testFile -Value "line one`nline two"
        }

        It 'Should return the content in upper case' {
            $result = Get-FileUpperCase -Path $script:testFile
            $result | Should -Be @('LINE ONE', 'LINE TWO')
        }
    }

    Context 'When the file does not exist' {
        It 'Should throw an error' {
            # The code to be tested is wrapped in a script block
            { Get-FileUpperCase -Path 'nonexistent.txt' } | Should -Throw 'File not found*'
        }
    }
}

By organizing your tests with this structure, you create a specification for your code that is not only automated but also serves as living documentation. Anyone can read your Pester tests and understand exactly what your code is designed to do and how it should handle different situations.