Testing Pester Code Coverage

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.

clip_image002

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 post 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.

clip_image004

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.

clip_image005

Here’s the corresponding Hello.Tests.ps1 script. Note that it tests only Get-Hello and only verifies that it returns “Hello, World.”

clip_image007

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.

clip_image009

In fact, if you comment-out the Else clause of Test-LeapYear, the code coverage result is surprising good.

clip_image011

Much too good, given that most of the script isn’t tested.

clip_image013

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 post 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. and a Windows PowerShell MVP. You can reach her at juneb@sapien.com or follow her on Twitter at @juneb_get_help.