Arrange–Act–Assert and Naming Tests People Can Read
- pester
- powershell
- testing
- aaa
- naming
- structure
A test you can read in five seconds is a test you'll trust at 2 a.m. when an alert fires and you're trying to understand what broke. Two simple habits get you there: the Arrange–Act–Assert pattern for the body, and a naming convention that says what the test proves. Neither requires new Pester features—just discipline about shape and words.
What you'll learn
- The Arrange–Act–Assert (AAA) pattern and why it makes tests scannable
- How AAA maps onto Pester's
BeforeAll/BeforeEachandIt - The
should <expected> when <condition>naming style - Why one behavior per test keeps names honest
The Arrange–Act–Assert pattern
Almost every good test has the same three movements:
- Arrange — set up the inputs and state the test needs.
- Act — perform the single action under test.
- Assert — check the result is what you expected.
That three-beat shape isn't mine to claim. We owe Arrange-Act-Assert to Bill Wake, who named it back in 2001. Few patterns have shaped how tests are written more than this one, and after all this time it's still the clearest structuring advice going. Thank you, Bill.
Keeping those phases in order, and separate, means a reader can find the one line that does the thing (Act) instantly, and see exactly what's claimed about it (Assert). Here's a function and a clean AAA test:
function ConvertTo-Slug {
param([string]$Text)
($Text.Trim().ToLower() -replace '[^a-z0-9]+', '-').Trim('-')
}
Describe 'ConvertTo-Slug' {
It 'should produce a hyphenated lowercase slug when given a spaced title' {
# Arrange
$title = ' Hello World Example '
# Act
$result = ConvertTo-Slug -Text $title
# Assert
$result | Should -Be 'hello-world-example'
}
}Three labeled beats, one action, one claim. You can read it aloud and it makes sense.
A messy test, refactored
Now compare a test that interleaves the phases — acting and asserting over and over, with setup sprinkled throughout:
# MESSY — everything tangled together
Describe 'ConvertTo-Slug' {
It 'works' {
(ConvertTo-Slug -Text 'A B') | Should -Be 'a-b'
$x = 'C D'
(ConvertTo-Slug -Text $x) | Should -Be 'c-d'
(ConvertTo-Slug -Text ' E ').Length | Should -BeGreaterThan 0
}
}This passes, but it's three different behaviors in one It called 'works'. When it fails, the report says 'works' failed — useless. The first failing line also hides the rest. Refactored into AAA with one behavior each:
Describe 'ConvertTo-Slug' {
It 'should join two words with a hyphen when given a single space' {
$result = ConvertTo-Slug -Text 'A B'
$result | Should -Be 'a-b'
}
It 'should collapse multiple spaces into one hyphen' {
$result = ConvertTo-Slug -Text 'C D'
$result | Should -Be 'c-d'
}
It 'should trim surrounding whitespace before slugifying' {
$result = ConvertTo-Slug -Text ' E '
$result | Should -Be 'e'
}
}Each test now has a name that doubles as documentation, and a failure points at exactly one behavior.
Mapping AAA onto Pester
The Arrange step often repeats across tests. Pester's setup blocks (from Part 10) are the natural home for it: shared, read-only setup goes in BeforeAll; per-test fresh state goes in BeforeEach. The It then holds just the Act and Assert.
Describe 'Get-Initials' {
BeforeAll {
# Arrange (shared): load the function once
function Get-Initials {
param([string]$FullName)
($FullName.Split(' ') | ForEach-Object { $_.Substring(0,1).ToUpper() }) -join ''
}
}
It 'should return two initials when given a first and last name' {
# Act
$result = Get-Initials -FullName 'ada lovelace'
# Assert
$result | Should -Be 'AL'
}
It 'should return three initials when given a middle name' {
$result = Get-Initials -FullName 'john quincy adams'
$result | Should -Be 'JQA'
}
}The shared Arrange lives once in BeforeAll; each It is a tidy Act/Assert pair. That's AAA, distributed across Pester's structure.
Naming: should when
A good test name describes the behavior, not the code. The most durable template is:
should <expected outcome> when <condition>It forces you to state both the expectation and the situation. This behavior-first, should-style of naming grew out of the behavior-driven-development movement that Dan North kicked off—describe what the code should do, not what it is, and the test starts to read like a promise. Credit where it's due. Compare these:
| Vague | Clear |
|---|---|
It 'test1' |
It 'should return 0 when the list is empty' |
It 'works' |
It 'should throw when the path does not exist' |
It 'checks the count' |
It 'should count three items when three are added' |
The clear versions read like a requirements doc. When one fails, the report tells a colleague — or future you — exactly what's no longer true, without opening the file.
A subtler benefit: writing the name first often reveals the test. If you can't fill in "should X when Y" cleanly, you don't yet know what behavior you're pinning down.
One behavior per test (watch for "and")
The word "and" in a test name is a warning sign. 'should save the file and send an email and return true' is three behaviors wearing one name. Split it into three Its. The payoff: a failure isolates to a single claim, and each name stays a true, single statement.
Try it yourself
Here are three vaguely named tests. Rename each to the should <expected> when <condition> form. The bodies already tell you what they check:
Describe 'Get-FileExtension' {
BeforeAll {
function Get-FileExtension { param([string]$Name) [System.IO.Path]::GetExtension($Name) }
}
It 'test extension' {
Get-FileExtension -Name 'report.pdf' | Should -Be '.pdf'
}
It 'no dot' {
Get-FileExtension -Name 'README' | Should -Be ''
}
It 'double' {
Get-FileExtension -Name 'archive.tar.gz' | Should -Be '.gz'
}
}Rewrite the three names so each reads as a sentence — for example, 'should return .pdf when the file has a single extension'. Run it with Invoke-Pester -Output Detailed and read the names back as a spec.
Common mistakes
Interleaving act and assert. Acting, asserting, acting again inside one
Ithides which step failed and buries the single action a reader is looking for. Keep Arrange, Act, and Assert in order and ideally one Act per test.Names that restate the code.
'should call ConvertTo-Slug'just narrates the line below it. Describe the outcome and condition —'should lowercase the result'— not the function name."And" in the name. It signals the test covers multiple behaviors. Split each into its own
Itso the name stays a single, honest claim.
Recap
Arrange–Act–Assert gives a test a predictable shape: set up, do the one thing, check the one result. Pester lets you push shared Arrange into BeforeAll/BeforeEach and keep each It lean. Pair that with should <expected> when <condition> naming and one behavior per test, and your suite becomes a spec a teammate can read without running it. Readable tests are trustworthy tests.
Next up: Part 12 — File Layout: .Tests.ps1, Folders, and Naming Conventions, where we organize tests across a whole project.