Skip to content
AgileLogics
6 min read

Testing with Pester

Prerequisites: Modules

Pester is PowerShell's testing framework. Tests describe expected behavior, and Pester checks it — so you can change scripts without fear.

Windows ships with the very old Pester 3.4; install the current version first:

# illustrative
Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser

A first test file

Test files end in .Tests.ps1:

Set-Content Get-Discount.ps1 @'
function Get-Discount {
  param([double]$Total)
  if ($Total -lt 0) { throw 'Total cannot be negative' }
  if ($Total -ge 100) { return $Total * 0.1 }
  0
}
'@

Set-Content Get-Discount.Tests.ps1 @'
BeforeAll {
  . $PSScriptRoot/Get-Discount.ps1
}

Describe 'Get-Discount' {
  It 'gives no discount under 100' {
    Get-Discount -Total 50 | Should -Be 0
  }

  It 'gives 10% at 100 or more' {
    Get-Discount -Total 200 | Should -Be 20
  }

  It 'rejects negative totals' {
    { Get-Discount -Total -5 } | Should -Throw 'Total cannot be negative'
  }
}
'@

$result = Invoke-Pester -Path ./Get-Discount.Tests.ps1 -PassThru -Output None
"Passed: $($result.PassedCount), Failed: $($result.FailedCount)"
Passed: 3, Failed: 0
  • BeforeAll runs once before the tests — here it dot-sources the script under test.
  • Describe groups tests; each It is one test.
  • Should makes assertions: -Be, -BeExactly, -BeGreaterThan, -Contain, -Match, -BeNullOrEmpty, -Throw, and more.
  • Run Invoke-Pester in a folder to run every *.Tests.ps1 file; the normal output lists each test with a green or red mark.

Test cases

Set-Content Math.Tests.ps1 @'
Describe 'Addition' {
  It 'adds <A> and <B> to get <Expected>' -ForEach @(
    @{ A = 1; B = 2; Expected = 3 }
    @{ A = -1; B = 1; Expected = 0 }
    @{ A = 2; B = 2; Expected = 5 }
  ) {
    $A + $B | Should -Be $Expected
  }
}
'@

$result = Invoke-Pester -Path ./Math.Tests.ps1 -PassThru -Output None
"Passed: $($result.PassedCount), Failed: $($result.FailedCount)"
$result.Failed | ForEach-Object { "$($_.ExpandedName): $($_.ErrorRecord[0].Exception.Message)" }
Passed: 2, Failed: 1
adds 2 and 2 to get 5: Expected 5, but got 4.

-ForEach runs one test per hash table; <A> placeholders in the name are filled in. The last case was deliberately wrong, to show a failure.

Mocking

Mock replaces a command during a test, so you can test logic that depends on things like the file system, the clock, or an API:

# illustrative
It 'reports when the service is down' {
  Mock Invoke-RestMethod { throw 'timeout' }
  Test-ServiceHealth | Should -Be 'DOWN'
  Should -Invoke Invoke-RestMethod -Times 1
}