Skip to main content

Measuring coverage

Twenty-four tests pass. That tells you the things you thought to test work. It says nothing about the code you forgot.

Code coverage answers a narrower question than people usually assume: which lines of my code ran while the tests ran? Not whether they were tested well — just whether they executed at all. Lines that never execute are definitionally untested, and that is worth knowing.

Turning it on

Coverage is a configuration setting, so it goes in test.ps1 alongside everything else:

test.ps1
$config = New-PesterConfiguration
$config.Run.Path = './Planetarium'
$config.Output.Verbosity = 'Detailed'
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = './testResults.xml'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './Planetarium'

Invoke-Pester -Configuration $config
./test.ps1
Tests completed in 870ms
Tests Passed: 24, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Processing code coverage result.
Covered 100% / 75%. 36 analyzed Commands in 7 Files.

Read that last line carefully — it trips people up. 100% is what you achieved; 75% is the target you are being measured against. The target is CodeCoverage.CoveragePercentTarget, which defaults to 75.

Set CodeCoverage.Path or you may measure nothing

Run.Path says which tests to run; CodeCoverage.Path says which code to measure. If you leave the second one out and run from a directory that does not contain your source, you get an empty report and a confusing 0%. Setting both explicitly costs one line and avoids a genuinely baffling afternoon.

Pester v6 uses a profiler-based tracer by default, which is fast enough to leave on routinely. If you need the older breakpoint-based collector, CodeCoverage.UseBreakpoints = $true brings it back — slower, and rarely what you want.

100% is not the finish line

The module is at 100%, and it would be a mistake to read that as "fully tested".

Coverage measures execution, not assertion. This test would give ConvertTo-AstronomicalUnit full coverage while checking nothing at all:

It 'Runs' {
InModuleScope Planetarium { ConvertTo-AstronomicalUnit -Kilometre 149597870.7 }
}

The line ran, so it is covered. Nothing was asserted, so it is untested. Coverage cannot tell those apart — which is why chasing a coverage number as a goal reliably produces tests that execute code without checking it.

What coverage is genuinely good at is the opposite direction: it finds code you forgot entirely. A 100% score is weak evidence of quality; an uncovered line is strong evidence of a gap.

Adding a feature

Time to create a real gap the way it actually happens — by adding a feature.

Export-PlanetReport currently overwrites without asking, which is unfriendly for something that writes files. Add a guard:

Planetarium/Public/Export-PlanetReport.ps1
function Export-PlanetReport {
[CmdletBinding()]
param (
[Parameter(Mandatory)]
[string] $Path,

[string] $Name = '*'
[string] $Name = '*',

[switch] $Force
)

$planets = @(Get-Planet -Name $Name)

if ($planets.Count -eq 0) {
throw "No planets matched '$Name'."
}

if ((Test-Path -Path $Path) -and -not $Force) {
throw "'$Path' already exists. Use -Force to overwrite it."
}

$report = foreach ($planet in $planets) {
'{0,-8} {1} AU' -f $planet.Name, (ConvertTo-AstronomicalUnit -Kilometre $planet.DistanceFromSunKm)
}

Set-Content -Path $Path -Value $report
}

Run the suite:

./test.ps1
Tests Passed: 24, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0

Still all green. Every existing test writes to a fresh path in TestDrive, so none of them hits the new branch — and none of them fails. Nothing in that output hints that you just shipped an untested code path.

Coverage does, and at Detailed verbosity it names the line:

Covered 95% / 75%. 40 analyzed Commands in 7 Files.
Missed commands:

File Class Function Line Command
---- ----- -------- ---- -------
Planetarium/Public/Export-PlanetReport.ps1 Export-PlanetReport 19 throw "'$Path' already exists. Use -Force to …
Planetarium/Public/Export-PlanetReport.ps1 Export-PlanetReport 19 throw "'$Path' already exists. Use -Force to …

File, function, line number and the source text of what never ran. That table is the whole reason to turn coverage on.

Before you move on

0/5

Next: finding the exact line and closing it.