Skip to main content

Grouping with Context

Fifteen tests, four files, and every file is a flat list of It blocks. That is fine at fifteen. It stops being fine somewhere around fifty, when "which of these tests are about wildcards again?" becomes a real question.

This page is about structure. No new behaviour gets tested — the suite still reports fifteen at the end — but it will be easier to read for the rest of the tutorial.

Context

Context groups related tests inside a Describe. It takes a name and a script block, exactly like Describe does, and it can hold its own setup.

The convention worth adopting: Describe names the thing, Context names the situation. Describe Get-Planet; Context "when called without a name".

Restructure Get-Planet.Tests.ps1:

Planetarium/Public/Get-Planet.Tests.ps1
BeforeAll {
Import-Module $PSScriptRoot/../Planetarium.psd1 -Force
}

Describe 'Get-Planet' {
Context 'When called without a name' {
It 'Returns all eight planets' {
(Get-Planet).Count | Should-Be 8
}

It 'Returns them in order from the sun' {
$names = (Get-Planet).Name
$names[0] | Should-Be 'Mercury'
$names[-1] | Should-Be 'Neptune'
}
}

Context 'When filtering by name' {
It 'Returns a single planet for an exact name' {
$planet = Get-Planet -Name 'Earth'
$planet.Name | Should-Be 'Earth'
$planet.Order | Should-Be 3
}

It 'Supports wildcards' {
(Get-Planet -Name 'M*').Name | Should-BeCollection @('Mercury', 'Mars')
}

It 'Returns nothing for an unknown planet' {
Get-Planet -Name 'Pluto' | Should-BeNull
}
}
}
./test.ps1
Describing Get-Planet
Context When called without a name
[+] Returns all eight planets 7ms
[+] Returns them in order from the sun 5ms
Context When filtering by name
[+] Returns a single planet for an exact name 6ms
[+] Supports wildcards 17ms
[+] Returns nothing for an unknown planet 1ms

Same five tests, now indented under the situation they belong to.

Notice what happened to the names. Returns all eight planets by default became Returns all eight planets — the "by default" moved into the Context, where it belongs. A test name should not have to restate its own conditions once a Context states them.

The setup and teardown family

You have been using BeforeAll since the first test. It has three siblings, and the difference is how often they run:

BlockRuns
BeforeAllOnce, before the first test in its block
BeforeEachBefore every test in its block
AfterEachAfter every test in its block
AfterAllOnce, after the last test in its block

All four can go in a Describe or a Context, and they apply to everything nested inside. That is why the single BeforeAll at the top of your file — importing the module — is still doing its job for tests that now live two levels deep.

The ordering is easiest to believe when you watch it. Drop this in a scratch file and run it:

scratch/Ordering.Tests.ps1
Describe 'Ordering' {
BeforeAll { Write-Host 'BeforeAll' }
BeforeEach { Write-Host ' BeforeEach' }
AfterEach { Write-Host ' AfterEach' }
AfterAll { Write-Host 'AfterAll' }

It 'first' { }
It 'second' { }
}
Invoke-Pester -Path ./scratch/Ordering.Tests.ps1 -Output Detailed
BeforeAll
Describing Ordering
BeforeEach
AfterEach
[+] first 23ms
BeforeEach
AfterEach
[+] second 1ms
AfterAll

BeforeAll runs once no matter how many tests follow; BeforeEach and AfterEach run once per test, bracketing each one.

The [+] lines look out of place — each appears after its own AfterEach rather than between the two. That is a reporting artifact, not an ordering one: Pester prints a test's result line only once the test is completely finished, and AfterEach is part of finishing. The Write-Host output is the honest record of the order.

Delete that file once you have seen it — it is a demonstration, not part of the suite.

Which one to reach for

Use BeforeAll for anything expensive or read-only — importing a module, loading fixture data. Use BeforeEach when a test would otherwise inherit state from the test before it.

Planetarium does not need BeforeEach yet, because every test builds what it needs and changes nothing. When Export-PlanetReport arrives in a later module and each test wants a fresh file path, that is exactly the case BeforeEach exists for.

Explaining a failure with -Because

Every Should-* assertion takes a -Because parameter. The text is folded into the failure message:

(Get-Planet).Count | Should-Be 8 -Because 'the solar system has eight planets'
[-] Returns all eight planets 77ms
Expected [int] 8, because the solar system has eight planets, but got [int] 9.

It earns its place when the expected value is a magic number whose origin is not obvious from the test. Should-Be 8 says what; -Because 'the solar system has eight planets' says why, to whoever is reading a red build at an unhelpful hour.

It is noise on an assertion that already explains itself, so use it sparingly rather than everywhere.

Before you move on

0/6

Next: running only some of them.