Skip to main content

Closing the gaps

Coverage says 95%, and the run has already told you which line it means.

Reading the missed commands

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 …

This table is printed for you whenever coverage is on and Output.Verbosity is Detailed — which test.ps1 already sets. There is nothing extra to run.

The line appears twice because Pester counts commands rather than lines, and that statement contains more than one — the throw and the string expansion inside it. Two entries, one gap.

This table is the single most useful thing coverage gives you. Reading it beats reading the percentage, because a percentage tells you that you have a problem while this tells you where it is.

Getting at the same data in code

The table is formatted for you. If you want the values themselves — for a build gate, a summary comment, a custom report — set Run.PassThru and read $result.CodeCoverage.CommandsMissed, which carries File, Line and Command per entry.

Run.PassThru is perfectly safe to keep in test.ps1, as long as you assign the result:

$result = Invoke-Pester -Configuration $config

An uncaptured result object is what causes trouble: anything a script leaves on the pipeline gets printed, so every run would end in a wall of formatted properties.

Writing the missing tests

The uncovered branch has two behaviours worth pinning: it refuses by default, and -Force overrides it.

Planetarium/Public/Export-PlanetReport.Tests.ps1
It 'Refuses to overwrite an existing report' {
$path = Join-Path $TestDrive 'existing.txt'
Export-PlanetReport -Path $path

{ Export-PlanetReport -Path $path } |
Should-Throw -ExceptionMessage "*already exists. Use -Force to overwrite it."
}

It 'Overwrites an existing report when -Force is used' {
$path = Join-Path $TestDrive 'forced.txt'
Export-PlanetReport -Path $path
Export-PlanetReport -Path $path -Name 'Earth' -Force

Get-Content -Path $path | Should-Be 'Earth 1 AU'
}

Both call Export-PlanetReport twice: once to create the file, once against the now-existing path. This is exactly the scenario every earlier test avoided by using a fresh filename each time — which is why the branch went uncovered.

The leading * in the expected message matters. The real message starts with the full TestDrive path, which is randomised per run, so matching it literally would fail. -ExceptionMessage accepts wildcards; match the stable part.

The second test asserts content, not just that no error occurred. Writing Earth over an eight-planet report and then checking the file holds exactly one line proves the overwrite really happened rather than the write being skipped.

Back to green

./test.ps1
Tests Passed: 26, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0
Covered 100% / 75%. 40 analyzed Commands in 7 Files.

Twenty-six tests, and no missed-commands table at all — when nothing is missed, Pester prints only the summary line.

A report file for CI

Like test results, coverage can be written to a file for other tools. Add it to test.ps1:

test.ps1
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './Planetarium'
$config.CodeCoverage.OutputPath = './coverage.xml'
coverage.xml
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<!DOCTYPE report PUBLIC "-//JACOCO//DTD Report 1.1//EN" "report.dtd"[]>
<report name="Pester ()">
<sessioninfo id="this" start="1784497576207" ...

The default format is JaCoCo, which most coverage services understand. Cobertura is the other option, via CodeCoverage.OutputFormat.

Add it to .gitignore alongside the test results:

testResults.xml
coverage.xml

Using coverage well

The habit worth forming is not "keep the number high". It is:

  1. Run coverage after adding a feature, not on a schedule.
  2. Read the missed commands, not the percentage.
  3. For each missed line, decide honestly — is this a gap, or code that does not need a test?
  4. Write tests for the gaps. Leave the rest.

Sometimes the right answer to an uncovered line is deleting it. Coverage is good at surfacing code that no longer has a reason to exist.

For the genuine exceptions there is an attribute that keeps a function out of the report entirely:

function Get-Something {
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage()]
param()
# ...
}

Before you move on

0/6

Last module: running all of this automatically on every push.