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:
$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.
CodeCoverage.Path or you may measure nothingRun.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:
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/5Next: finding the exact line and closing it.