Skip to content
Updated 7 min read

Setup & Teardown: BeforeAll, AfterAll, BeforeEach, AfterEach

  • pester
  • powershell
  • testing
  • beforeall
  • discovery
  • structure

📦 Run it yourself: download the companion code pack; this part's examples are in the 03-structure/ folder (Pester 5-verified).

Pester 5 runs your test file in two distinct phases, and it trips up almost everyone exactly once, usually as a baffling "command not found" on a function you clearly defined two lines up. The fix is simple once you see how it works. This part explains the Discovery vs Run phases and the four blocks (BeforeAll, AfterAll, BeforeEach, AfterEach) that put setup in the right place.

What you'll learn

  • The Pester 5 Discovery vs Run phase model and why it exists
  • Why setup code belongs in BeforeAll, never at the top of the file
  • How BeforeEach/AfterEach give each test a clean slate
  • How variables defined in setup blocks are scoped to the tests inside

The two-phase model (the part that trips everyone up)

When you run Invoke-Pester, Pester does not just execute your file top to bottom. It runs it in two passes:

  1. Discovery phase: Pester executes the file to find all your Describe, Context, and It blocks and build a map of the suite. During this pass, the script blocks inside It are not run; Pester is only collecting structure. BeforeAll/BeforeEach bodies do not run here either; Pester just registers them.
  2. Run phase: Pester walks the map it built and executes the tests: it runs each BeforeAll, then the It bodies, with BeforeEach/AfterEach around them, then AfterAll.

Here's the catch. Any bare code you write at the top of the file (outside any block) runs during Discovery. And the contents of It run during Run. Those are two different moments, and Discovery happens first, for the whole file, before any test executes.

That timing gap is the source of nearly every beginner Pester 5 mystery.

The classic "command not found"

Watch this fail. It looks completely reasonable. Save it as Get-Square.Tests.ps1 next to the Get-Square.ps1 shown in the next section, and run it in a fresh PowerShell session (if Get-Square is already defined in your session from earlier experiments, the test can still find it and pass):

# Fails on purpose: don't copy this pattern. The bare dot-source runs during Discovery.
. $PSScriptRoot/Get-Square.ps1   # bare line at top of file

Describe 'Get-Square' {
    It 'squares 3 to 9' {
        Get-Square 3 | Should -Be 9
    }
}

You'd expect the dot-source to load Get-Square so the test can call it. But trace the phases:

  • During Discovery, the bare line runs and loads the function, but into the Discovery scope.
  • During Run, Pester executes the It body in a fresh scope where that function isn't available. You get The term 'Get-Square' is not recognized. (Windows PowerShell 5.1 also adds a Suggestion [3,General] line saying the command exists in the current location. That's PowerShell's own hint, and you can ignore it here.)

The setup ran at the wrong time: in Discovery, while the test runs in Run.

The fix: put setup in BeforeAll

BeforeAll is a block whose body runs during the Run phase, once, before the tests in its container. Move the dot-source inside it and the timing lines up:

# Get-Square.Tests.ps1
Describe 'Get-Square' {
    BeforeAll {
        . $PSScriptRoot/Get-Square.ps1
    }

    It 'squares 3 to 9' {
        Get-Square 3 | Should -Be 9
    }
}

Now the function loads in the Run phase, in scope for the tests. To make this runnable, create Get-Square.ps1 next to the test file:

# Get-Square.ps1
function Get-Square {
    param([double]$Number)
    $Number * $Number
}

Then run Invoke-Pester ./Get-Square.Tests.ps1. Green. The rule that follows: anything the tests depend on goes in BeforeAll, not at the top of the file.

Variable scoping from BeforeAll

Variables you assign in BeforeAll are available to every It, BeforeEach, and nested block inside the same container. This is how you share expensive or fixed setup:

# OrderTotals.Tests.ps1
Describe 'Order totals' {
    BeforeAll {
        # Runs once, before all tests in this Describe
        $catalog = @{ apple = 0.50; bread = 2.00 }
    }

    It 'prices a known item' {
        $catalog['apple'] | Should -Be 0.50
    }

    It 'knows the catalog size' {
        $catalog.Count | Should -Be 2
    }
}

Variables defined in BeforeAll flow down into the tests. They don't flow upward or sideways: a variable created in a BeforeEach or inside one It won't be visible in AfterAll, and a variable from one Describe isn't visible in the next Describe. Setup state is shared downward, not collected back up.

BeforeEach and AfterEach: a clean slate per test

BeforeAll runs once. BeforeEach runs before every It in its container, and AfterEach runs after every It. Use them when tests would otherwise pollute each other through shared mutable state:

# ShoppingCart.Tests.ps1
Describe 'ShoppingCart' {
    BeforeEach {
        # Fresh cart before each test, so tests can't leak into one another
        $cart = [System.Collections.Generic.List[string]]::new()
    }

    It 'starts empty' {
        $cart.Count | Should -Be 0
    }

    It 'holds an item after adding one' {
        $cart.Add('apple')
        $cart.Count | Should -Be 1
    }
}

If you'd built the cart once in BeforeAll, both tests would share one cart, and the result would depend on their order: put holds an item after adding one first, and starts empty fails with Expected 0, but got 1. BeforeEach guarantees isolation: each test gets its own fresh cart.

AfterAll and AfterEach: cleanup

AfterEach runs after each test; AfterAll runs once after all tests, even if a test failed. Use them to undo side effects: close a connection, remove a temp file, reset a variable.

# MarkerFile.Tests.ps1
Describe 'temporary marker file' {
    BeforeAll {
        $markerPath = Join-Path $TestDrive 'marker.txt'
        'hello' | Set-Content -Path $markerPath
    }

    It 'created the file' {
        Test-Path $markerPath | Should -BeTrue
    }

    AfterAll {
        # Tidy up once, after every test in this Describe has run
        if (Test-Path $markerPath) { Remove-Item $markerPath }
    }
}

($TestDrive is a Pester-provided sandbox path that's cleaned up automatically; Part 14 covers it in full. The AfterAll cleanup isn't strictly needed here, but it shows where teardown goes.)

Try it yourself

Take the broken pattern from earlier and fix it. Create a file Greeter.ps1:

# Greeter.ps1
function Get-Greeting {
    param([string]$Name)
    "Hello, $Name!"
}

Now write Greeter.Tests.ps1 with the dot-source at the top of the file (bare) and a test that calls Get-Greeting. Run it and watch the "command not found." Then move the dot-source into a BeforeAll and run it again. The test should pass. You've just felt the Discovery/Run boundary firsthand.

Common mistakes

Top-level code that you expect tests to use. Bare lines run in Discovery, before any test. Anything a test depends on (dot-sourcing, imports, building fixtures) belongs in BeforeAll.

Expecting BeforeEach variables in AfterAll. State flows down into tests, not back up into teardown. A variable born in BeforeEach or an It is gone by AfterAll. Capture what AfterAll needs in BeforeAll instead.

Expensive setup in BeforeEach. If building the fixture is slow (a database connection, a large file) and the tests don't mutate it, use BeforeAll to do it once. Reserve BeforeEach for state that must be reset between tests.

Recap

Pester 5 discovers your tests first, then runs them: two phases, in that order. Bare top-level code runs during Discovery, while It bodies and the Before*/After* blocks run during Run. Put setup in BeforeAll (once) or BeforeEach (per test), clean up in AfterAll/AfterEach, and remember that setup variables flow downward into your tests.

Next up: Part 11, Arrange-Act-Assert and Naming Tests People Can Read, where good structure meets readable intent.

Posts here are written with AI assistance and tested against real code before publishing.

Comments