Testing Pester Code Coverage
- Details
- Written by June Blender
- Last Updated: 09 October 2019
- Created: 24 June 2016
- Hits: 12283
Applies to: PowerShell 5.0.10586.122, 5.1.14367, Pester 3.4.0
I’m one of those people who thinks a score of 99% is failing, so I love to see those 100% scores when I use the CodeCoverage parameter of Invoke-Pester.
But, while assembling my Pester presentations for DevOps Global Summit 2016 and PowerShell Conference Europe 2016, I realized the 100% code coverage score means that 100% of my code (every line) ran during the test. It doesn’t mean that 100% of my code is tested. Code coverage reports are really valuable, but you need to understand what they test and how to use them.
This article is the fourth in a series about how to run Pester tests.
See the posts in this Pester series:
What is code coverage?
Among the many amazing features of the Pester test framework for PowerShell is a code coverage test. To run it, you use the CodeCoverage parameter of Invoke-Pester.
So, what’s code coverage?
“… describe(s) the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage…”
Wikipedia: “code coverage”
Ah, it’s a measure of the code testing.
But, at least in this case, I found out that it’s a measure of the code that runs during a test; not the degree to which the code is tested or anything about the quality of the test.
Invoke-Pester -CodeCoverage
The CodeCoverage parameter of Invoke-Pester adds a code coverage report to the tests that Pester runs. A code coverage report lists the lines of code that did and did not run during a Pester test.
Code coverage report: Covered 77.78 % of 9 analyzed commands in 1 file. Missed commands: File Function Line Command ---- -------- ---- ------- Hello.ps1 Test-LeapYear 30 Write-Warning "$($date.Year) is not a leap year." Hello.ps1 Test-LeapYear 30 $date.Year
It seems like -CodeCoverage should be a switch parameter, but it requires strings and/or hash table values. In its simplest form, it takes a path string. But, be careful! Enter the path to the code being tested; not the path to the test file.
For example, if you have Get-Script.ps1 and its test file, Get-Script.Tests.ps1, the value of the Script parameter is Get-Script.Tests.ps1, but the value of the CodeCoverage parameter is Get-Script.ps1.
This makes sense. You want the Code Coverage report to make sure the code in your script ran. The primary output already tells you which tests ran.
By default, Invoke-Pester writes the code coverage report to the host program (like Write-Host), so you can’t save it in a variable, redirect it, or pipe it to a file.
To save your Code Coverage report, use the PassThru parameter. The custom object that Invoke-Pester returns has an additional CodeCoverage property that contains a custom object with detailed results of the code coverage test, including lines hit, lines missed, and helpful statistics.
You can pipe the code coverage custom object to Export-Clixml or to ConvertTo-Json and then to Out-File. However, NUnitXML and LegacyNUnitXML output that the OutputXML and OutputFormat parameters of Invoke-Pester use do not include any code coverage information, because it’s not supported by the schema.
Oh, and if you use the Quiet parameter to suppress the host output of Invoke-Pester, it suppresses the Code Coverage report, too.
What does the Code Coverage parameter do?
To identify what the CodeCoverage parameter tests, let’s experiment.
Here’s a little test script, Hello.ps1. It consists of three functions:
- Get-Hello function writes ‘Hello, World” and calls the Test-LeapYear function.
- Test-LeapYear calls a Get-DateHelper function, then calls the IsLeapYear static method.
- Get-DateHelper calls the Get-Date cmdlet. I added Get-DateHelper so I could mock it and provide varying test input to Test-LeapYear.
Here’s the corresponding Hello.Tests.ps1 script. Note that it tests only Get-Hello and only verifies that it returns “Hello, World.”
So, you would expect the Code Coverage report to be pretty dismal, but it’s not too bad. It just reports that the Else clause of Test-LeapYear didn’t run. But, it doesn’t report the functionality of Test-LeapYear isn’t tested at all and Get-DateHelper aren’t tested at all.
In fact, if you comment-out the Else clause of Test-LeapYear, the code coverage result is surprising good.
Much too good, given that most of the script isn’t tested.
The Code Coverage report lists the lines of code in the script that ran (and didn’t run) during the test. In the first version, the Else clause never ran because the current year, 2016, is a leap year.
It does not evaluate the tests. It doesn’t verify that the lines of code are tested; only that they ran during the test.
Notice that this example uses Write-Warning, which writes to the warning stream, not Write-Output. If I use Write-Output, or any other cmdlet that writes to the output stream (stdout), the current simplistic test for Get-Hello would fail, but CodeCoverage would still be 100%.
Customizing Code Coverage
By default, the CodeCoverage parameter evaluates entire script files, but you can limit it to specific functions or even lines in the files. To customize your CodeCoverage value, use a hash table.
For example, this command evaluates code coverage in the Hello.ps1 file while running tests in the Hello.Tests.ps1 file. (You can also include a string array value with wildcard characters, but this is a simple case.)
Invoke-Pester -Script Hello.Tests.ps1 -CodeCoverage Hello.ps1 |
This command evaluates only for the Test-LeapYear function.
Invoke-Pester -Script Hello.Tests.ps1 -CodeCoverage @{Path = 'Hello.ps1'; Function = 'Test-LeapYear'} |
This command evaluates only for the functions in Hello.ps1 with the Get verb.
Invoke-Pester -Script Hello.Tests.ps1 -CodeCoverage @{Path = 'Hello.ps1'; Function = 'Test-LeapYear'} |
You can be very specific. This command evaluates only lines 23 – 26 in Hello.ps1. The defaults for StartLine and EndLine are the first and last lines of the script, respectively.
Invoke-Pester -Script Hello.Tests.ps1 -CodeCoverage @{Path = 'Hello.ps1'; StartLine = 23; EndLine = 26 } |
And, you can use aliases for the Path (P), Function (F), StartLine (S), and EndLine (E) keys, although I wouldn’t do that. As a best practice, I omit aliases and abbreviations in shared code.
Invoke-Pester -Script Hello.Tests.ps1 -CodeCoverage @{P = 'Hello.ps1'; F = 'Test-LeapYear'} |
Use Code Coverage
So, in summary, use the CodeCoverage parameter — I do! — but don’t get too excited about a 100% result, or any surprisingly positive or negative result. Always test your tests and, in the immortal words of whomever Dave Wyatt quotes, “Never trust a test that won’t fail.”
Learning Pester? Check out Real-World Test-Driven Development with Pester. The code and slides are in Github at https://github.com/juneb/PesterTDD.
This article is the fourth in a series about how to run Pester tests. See also: How to Run Pester Tests, Invoke-Pester: Run Selected Tests, and How to Pass Parameters to a Pester Test Script.
June Blender is a technology evangelist at SAPIEN Technologies, Inc. You can reach her at This email address is being protected from spambots. You need JavaScript enabled to view it. or follow her on Twitter at @juneb_get_help.
For licensed customers, use the forum associated with your product in our Product Support Forums for Registered Customers.
For users of trial versions, please post in our Former and Future Customers - Questions forum.