Closing the gaps
Coverage stands at 95%, and the run has already told you which lines to look at.
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 set to Detailed, which test.ps1 already does.
The line appears twice because Pester counts throw keyword as one command, and the message string as another. This is a special case for throw.
Now we know which code we need to test next.
You can create your own report using the result object (Run.PassThru) and processing the data in $result.CodeCoverage.CommandsMissed.
$config.Run.PassThru = $true
$result = Invoke-Pester -Configuration $config
# StartColumn will confirm the two missed commands above are in fact different. One entry for `throw` and one for the message string
$result.CodeCoverage.CommandsMissed | Format-Table Function, Line, StartColumn, Command
Writing the missing tests
The uncovered branch has two behaviours worth pinning: it refuses by default, and -Force overrides it. Add both tests at the bottom of the Describe block, below the four you already have:
Describe 'Export-PlanetReport' {
# ... the four existing It blocks ...
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, and once more to hit the file exists error.
The exception message starts with the filepath which is randomized per run, so a wildcard * is used to match the stable part and confirm it's the correct exception.
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.
Back at 100% coverage. Enjoy this rare moment.
A report file for CI
When Code Coverage is enabled it writes a report to ./coverage.xml by default that can be used by CI systems and other coverage reporting tools. You control the path using the CodeCoverage.OutputPath option - we'll just set the default explicit:
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = './Planetarium'
$config.CodeCoverage.OutputPath = './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.
Like the test results file, it is a build artifact — if your project is under version control, .gitignore it alongside testResults.xml.
Using coverage well
The habit worth forming is not "keep the number high". It is:
- Run coverage after adding a feature, not on a schedule.
- Read the missed commands, not the percentage.
- For each missed line, decide honestly — is this a gap, or code that does not need a test?
- 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/6Last module: running all of this automatically on every push.