Skip to content
6 min read

Describe vs Context vs It: Organizing Intent

  • pester
  • powershell
  • testing
  • describe
  • context
  • structure

Three nouns, one job each. Get Describe, Context, and It right and your test file stops being a pile of assertions and starts reading like a spec you could hand to a teammate. Get them wrong and you end up with one giant test that fails for five different reasons and tells you nothing meaningful. This post is about giving each keyword the role it was designed for.

What you'll learn

  • What Describe, Context, and It each mean and when to reach for them
  • How nesting works and why the output indents to mirror your structure
  • How to split a flat list of tests into meaningful scenarios
  • The structural mistakes that make a suite hard to read

The three building blocks

Pester gives you three containers, and the trick is matching each to the right level of intent:

  • Describe — the thing under test: a function, a feature, or a unit of behavior. It is the top-level group.
  • Context — a scenario or state: "when the input is valid", "when the file is missing", "when the user is an admin". It groups tests that share a situation.
  • It — a single observable behavior: one claim, ideally one assertion. This is where Should lives.

Read top to bottom, a good file says: for this function, in this situation, it does this specific thing.

A flat suite, and why it's hard to read

Here is a small function and a first pass at testing it. It works, but everything is jammed into one flat list.

function Get-Discount {
    param([int]$Quantity)
    if ($Quantity -lt 1) { throw 'Quantity must be at least 1' }
    if ($Quantity -ge 10) { return 0.20 }
    return 0.0
}

Describe 'Get-Discount' {
    It 'returns 0 for a single item' {
        Get-Discount -Quantity 1 | Should -Be 0.0
    }
    It 'returns 0 for nine items' {
        Get-Discount -Quantity 9 | Should -Be 0.0
    }
    It 'returns 0.20 for ten items' {
        Get-Discount -Quantity 10 | Should -Be 0.20
    }
    It 'throws on zero' {
        { Get-Discount -Quantity 0 } | Should -Throw
    }
}

Save that as Get-Discount.Tests.ps1 and run Invoke-Pester ./Get-Discount.Tests.ps1. It passes. But notice the tests are mixing two different stories: "valid quantities and their discounts" and "invalid input throws." A reader has to squint to see the structure.

Introducing Context to group scenarios

Context lets you name the situation each group of tests lives in. The same tests, reorganized:

Describe 'Get-Discount' {
    Context 'when the quantity is valid' {
        It 'returns no discount for a single item' {
            Get-Discount -Quantity 1 | Should -Be 0.0
        }
        It 'returns no discount just below the bulk threshold' {
            Get-Discount -Quantity 9 | Should -Be 0.0
        }
        It 'returns a 20% discount at the bulk threshold' {
            Get-Discount -Quantity 10 | Should -Be 0.20
        }
    }

    Context 'when the quantity is invalid' {
        It 'throws for a quantity of zero' {
            { Get-Discount -Quantity 0 } | Should -Throw
        }
        It 'throws for a negative quantity' {
            { Get-Discount -Quantity -5 } | Should -Throw
        }
    }
}

Now the file reads as a specification: Get-Discount — when the quantity is valid, it does X, Y, Z; when invalid, it throws. The grouping is documentation.

Output mirrors your structure

Run the reorganized file with detailed output and Pester indents the result to match your nesting:

Invoke-Pester ./Get-Discount.Tests.ps1 -Output Detailed

You'll see something like:

Describing Get-Discount
 Context when the quantity is valid
   [+] returns no discount for a single item
   [+] returns no discount just below the bulk threshold
   [+] returns a 20% discount at the bulk threshold
 Context when the quantity is invalid
   [+] throws for a quantity of zero
   [+] throws for a negative quantity

That indentation is not decoration. When a test fails, the path tells you exactly which function, in which scenario, broke which behavior — before you even read the error.

Nesting: how deep is too deep?

Context can nest inside Context, and Describe can nest too. Use it sparingly. A second level is occasionally useful — for example, "when the user is an admin" containing "and the resource is locked." But three or more levels usually means you're describing combinations that would read better as separate, well-named Its or as data-driven tests (a later post in this series).

A reliable rule: if you can't read the nested path aloud as a sentence that makes sense, flatten it.

Try it yourself

Take this flat suite and reorganize it into two Context blocks — one for found, one for not found:

function Get-UserRole {
    param([hashtable]$Users, [string]$Name)
    if ($Users.ContainsKey($Name)) { return $Users[$Name] }
    return 'guest'
}

Describe 'Get-UserRole' {
    It 'returns the role for a known user' {
        Get-UserRole -Users @{ ada = 'admin' } -Name 'ada' | Should -Be 'admin'
    }
    It 'returns guest for an unknown user' {
        Get-UserRole -Users @{ ada = 'admin' } -Name 'bob' | Should -Be 'guest'
    }
    It 'returns guest when the table is empty' {
        Get-UserRole -Users @{} -Name 'ada' | Should -Be 'guest'
    }
}

Wrap the first It in a Context 'when the user exists' and the other two in a Context 'when the user is not found'. Run it with -Output Detailed and confirm the indentation matches your intent.

Common mistakes

One giant It testing five things. If an It has five Should lines covering unrelated claims, the first failure hides the rest and the name can't honestly describe what it checks. Split it — one behavior per It.

Context that names nothing. Context 'tests' or Context 'stuff' adds a level of nesting with zero meaning. A Context should always answer "in what situation?" — usually starting with "when."

Over-nesting. Four levels of Describe/Context produce output nobody can scan. If the path reads like a maze, flatten it.

Recap

Describe names the unit, Context names the scenario, and It names a single behavior. Used well, they turn a flat list of assertions into a readable spec, and Pester's indented output mirrors that structure so failures point you straight to the broken behavior. The goal isn't more keywords — it's intent you can read at a glance.

Next up: Part 10 — Setup & Teardown: BeforeAll, AfterAll, BeforeEach, AfterEach, where we meet the Pester 5 phase model that surprises everyone exactly once.