Contents
- Hello, World, Pester-Style
- Executing Tests
- Writing Unit Tests
- Test Anatomy
- Pester Command Summary
- Conclusion
PowerShell has grown up. In its recent versions, there are few things that PowerShell applications aren’t capable of doing just as easily as a more traditional language, plus a myriad of tasks that it can do better. It particularly shines at automating routine tasks. And even for these, there is a lot of sense in being able to do automated unit testing when developing and modifying scripts. This isn’t just a developer obsession: anyone who is writing scripts that must be reliable can do it faster, and with less tedium, by putting in place a safety net of unit tests. Unit tests provide maintainability (allowing you to make changes knowing that you have not broken anything), documentation (the tests show you and your colleagues how to use your scripts), and robustness (writing test cases is a good way to find holes in your logic) . And with the recent advent of unit testing frameworks now available for PowerShell, it is easier than ever to get started. When I needed to do this, I looked around at the handful of nascent unit testing frameworks available and quickly settled on Pester, the subject of this article.
It is important to have a good grasp of the mechanics of unit tests in PowerShell. As with many developer tools, frameworks, etc., you have some reference material provided with the product, and you have some usage examples, but what it lacks is a practical, no-nonsense description of how to use the thing. So, what you will find herein is what I like to call “tips from the trenches”: a collection of user tips, techniques, and tools accumulated and developed through hard-fought battles, which I can relate to you here so that you do not have to struggle with the same issues. I’m hoping that this will significantly reduce the Pester learning curve for you.
Pester has, to be fair, made my task slightly less urgent. Pester 3.0 was just released; besides , by adding some new functionality they also fixed a couple of issues that I now no longer need workarounds for: Pester, for example, now supports PowerShell’s ‘strict’ mode), so I definitely recommend the 3.0 release.
Hello, World, Pester-Style
Before getting into the details, though, let’s take a look at how to write a unit test with Pester. Almost everything in this section is explored in more detail throughout the remainder of this series, but let’s start with the canonical “Hello, World”. See the next section Executing Tests for instructions on installing and importing the Pester library, but if you have PSGet installed, it is as simple as typing Install-Module Pester
to download and install the pester package and then Import-Module Pester
at the start of the script to load it into the current session.
Step 1: Create a subdirectory for your PowerShell program.
The next step, that of creating a file to hold your source code (HelloWorld.ps1) and a file to hold your test code (HelloWorld.Tests.ps1), is done automatically by executing the following –
1 |
PS> New-Fixture -Name HelloWorld |
You will see that Pester has checked to see if the files exist and, as they have not, has created these two files, HelloWorld.ps1 and HelloWorld.Tests.ps1 for you, and put in sufficient skeletal code to run the unit test, so let’s just invoke Pester to test the empty function:
1 2 3 4 5 6 7 8 9 10 |
PS> invoke-pester Executing all tests in 'C:\usr\tmp' Describing HelloWorld [-] does something useful 1.13s Expected: {False} But was: {True} at line: 7 in C:\usr\tmp\HelloWorld.Tests.ps1 Tests completed in 1.13s Passed: 0 Failed: 1 |
The first test was expecting to see false
but saw true
, so the test failed. If you open the test file HelloWorld.Tests.ps1,
you will see what I call the file preamble, which I will ignore for now (described later under Test Anatomy), followed by the actual test which should be this:
1 2 3 4 5 |
Describe "HelloWorld" { It "does something useful" { $true | Should Be $false } } |
In a nutshell, the Describe
command specifies the name of the function under test and the It
command specifies a single test. The content of that test is the single line of code shown that maps to this syntax:
actual-value | Should Be expected-value
Want to see what a passing test looks like? Change the actual value to $false
(or the expected value to $true
), then run it again.
Now let’s write some real code, something that actually invokes the function in the source file. Open the source file and you will find an empty function:
1 2 3 |
function HelloWorld { } |
Let’s change it to simply return a phrase:
1 2 3 |
function HelloWorld { return "Hello from Pester" } |
And now let’s write a test that confirms that return value; I am just going to add this as a second test so you can see how a Describe
block may contain several tests. We leave the preamble in place
1 2 3 4 5 6 7 8 9 |
Describe "HelloWorld" { It "does something useful" { $true | Should Be $true } It "returns a canonical phrase" { HelloWorld | Should Be "Hello from Pester" } } |
Notice also that I changed the original dummy test expect $true
, so both tests should pass when you execute invoke-pester
:
1 2 3 4 5 6 7 |
PS> invoke-pester Executing all tests in 'C:\usr\tmp' Describing HelloWorld [+] does something useful 58ms [+] returns a canonical phrase 31ms Tests completed in 89ms Passed: 2 Failed: 0 |
What if we want a more elaborate ‘HelloWorld’ routine that says ‘hello’ to whatever name that you specify? To do this, we need to introduce a parameter to our HelloWorld
function: we will pass in a name, if no name is given we default to “Pester” as we did above; and this time let’s even write the tests first in the HelloWorld.Tests.ps1
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Describe "HelloWorld" { It "does something useful" { $true | Should Be $true } It "with no input returns a canonical phrase" { HelloWorld | Should Be "Hello from Pester" } It "with a name returns the standard phrase with that name" { HelloWorld "Venus" | Should Be "Hello from Venus" } It "with a name returns something that ends with name" { HelloWorld "Mars" | Should Match ".*Mars" } } |
I have revised the description of the second test and added two new tests. The second test passes no parameter, so we expect it to use the default “Pester”. The new tests pass a name so we expect that to be in the returned value. Just to give you a hint at the flexibility of the Should
command, I am showing two versions, one with Be
and one with Match
that compares an expected value to an actual value with a regular expression.
If you run the test file now, the new tests will fail, because we have not written any code to support them. So, finally, modify the source file with an eye to make those tests pass:
1 2 3 |
function HelloWorld($name='Pester') { return "Hello from $name" } |
Running Pester confirms success:
1 2 3 4 5 6 7 8 9 |
PS> invoke-pester Executing all tests in 'C:\usr\tmp' Describing HelloWorld [+] does something useful 65ms [+] with no input returns a canonical phrase 26ms [+] with a name returns the standard phrase with that name 5ms [+] with a name returns something with that name 9ms Tests completed in 106ms Passed: 4 Failed: 0 |
Executing Tests
First just a brief description of running tests; then on to the main agenda of creating tests.
Installing Pester
To install, simply copy the entire Pester distribution folder into your standard modules folder (i.e. $env:USERPROFILE\Documents\WindowsPowerShell\Modules
).
Run this from the command-line (or add to your profile) to enable pester in your PowerShell session:
1 |
PS> Import-Module Pester |
Running Pester
Use the Invoke-Pester command to run your unit tests. A test file is identified merely by its name; any file matching *.Tests.ps1
qualifies. There are several ways to run your unit tests with Pester.
- (1)
- With no arguments,
Invoke-Pester
examines the tree of files rooted in your current directory and executes all test files it finds: So you can just change directory (Set-Location
) to the appropriate place and run with no arguments.
1PS> Invoke-Pester - (2)
- Specify a path and
Invoke-Pester
examines the tree rooted at that specified location.
1PS> Invoke-Pester -Path tests\MyProject\ScriptCmdlets - (3)
- Run one or more specific tests by naming them (wildcards allowed). The
TestName
parameter corresponds to Pester’sDescribe
block, which is a container for a logical group of tests. Thus, this will run tests whoseDescribe
blocks are namedGetFilesMany
,GetFiles
, orGetFilesLackingProjectName
, for example:
1PS> Invoke-Pester -TestName 'GetFiles*'
If you do not
use the -Path parameter, Pester uses the current directory.
Continuous Integration
There are two useful options to Invoke-Pester
for continuous integration purposes:
You can ask Pester to exit with a return code equal to the number of failed tests with the -EnableExit
parameter. Without this flag your tests will, for all intents and purposes, always be passing!
1 |
PS> Invoke-Pester -EnableExit |
But watch out: you do not want to use -EnableExit
when running interactively! Yes, it returns an exit code but it terminates the current shell as well. When used judiciously, that is fine running a build in batch mode. But interactively it will close your current window.
You can ask Pester to output an NUnit-compatible result file with the -OutputXml
parameter.
1 |
PS> Invoke-Pester -OutputXml 'logs\mylogfile.xml' |
You can use the -EnableExit flag in a continuous build, but don’t try to use it when running Invoke-Pester interactively.
Writing Unit Tests
Naming your Test File Name
Typically, you create a single test file corresponding to a single source code file. The canonical name of this test file simply adds “.Tests” before the “.ps1” in the name of the file under test. So for this code file…
\src\MyProject\ScriptCmdlets\GetStuff.ps1
…this Pester file contains the tests:
1 |
\tests\MyProject.Test\ScriptCmdlets\GetStuff.Tests.ps1 |
There are different schemes for where to put your test files. For scripting languages, some people put a test file in the same directory as the source file; but compiled languages tend to separate the source file tree from the test file tree, and I see no reason to dismiss that clean separation just because PowerShell is a scripting language. Nonetheless, pick the style you prefer and stick with it; it is significant in the next section. (By the way, Pester’s New-Fixture
command can streamline creating the file for you.)
Making your Test File Aware of Your Source File
Each test file must begin with a preamble that specifies the source file upon which to operate. The default preamble will serve most purposes, but if you have a special requirement then you can alter the preamble to accommodate it.
If you are testing a single .ps1 script file...
… then you simply need to dot-source that file. With the source and test file given just above the top of your test file should point to the source file with a command similar to this-note the important dot and space at the start of the line:
. ‘..\..\..\src\MyProject\ScriptCmdlets\GetStuff.ps1’
While that will certainly work, it is fragile: if you rename or move the source file or test file it will break. This two-line sequence is more robust; you may wish to tweak it to your own conventions, but this one works for the convention I specified above, namely with a source file file.ps1
in a project named Project in the src
tree then the corresponding test file is file.Tests.ps1
in a project named Project.Test
in the tests
tree:
1 2 3 4 5 |
$srcFile = $MyInvocation.MyCommand.Path ` -replace 'MySolution\\tests\\(.*?)\.Test\\(.*?)\.Tests\.ps1', ` 'MySolution \src\$1\$2.ps1' . $srcFile |
If you are testing a function within a module...
… then you just import the module as you normally would with the Import-Module
cmdlet. If the module is installed in a standard PowerShell location (i.e. in the search path pointed to by $env:PSModulePath
), the module name will suffice, e.g.
Import-Module MyModule
If, on the other hand, your module is in an arbitrary location in your source files in your source tree, then provide the path name. I would again recommend leveraging your own conventions to divine the source module, e.g. perhaps something like this:
1 2 3 4 5 |
$srcModule = $MyInvocation.MyCommand.Path ` -replace 'MySolution\\tests\\(.*?)\.Test\\(.*?)\.Tests\.ps1', ` 'MySolution \src\$1\$2.psd1' Import-Module $srcModule |
Your test must bring into scope your script or module under test.
Making your Test File Aware of Helper Files
It is likely that you will want to develop some testing support functions as you build out your unit tests, functions that help you streamline your test code. I keep these helper functions in a separate subdirectory just below my test scripts folder. Thus, my test file also includes this in the preamble:
1 2 |
$helperDir = "$PSScriptRoot\TestHelpers" Resolve-Path $helperDir\*.ps1 | % { . $_.ProviderPath } |
That sequence will bring into the current scope all of the helper functions in my TestHelpers
directory. By using Resolve-Path
and the wildcard on the file name, any new functions that I might develop simply have to be placed in that directory and they are automatically brought into the current scope on the next run.
Test Anatomy
The prior discussion focused on some key details to keep in mind when creating tests; now let’s take a look at the anatomy of a test file itself. In the introduction, you saw about the simplest possible test, using a Describe
command to identify the function under test and an It
command to specify an individual test. The New-Fixture
command that created the test template for you also included what I call the ‘file preamble’, which I told you to just ignore at that point. But to build your real unit tests, you must inevitably get a good grounding in the whole syntax. This section tackles that, before taking you further into tips and techniques for getting the most from Pester.
Take a look at the code skeleton below; it shows a composite view including all optional and enumerable components; once you are familiar with all the pieces this will not look so daunting. It can be immediately helpful with what you have learned so far. Right at the top you can see the <file-preamble>
. A couple lines lower you see the Describe
. If you are dealing with PowerShell modules, you will need the InModuleScope
but our earlier test was not using modules, which is why we went straight to the Describe
command. Skip a few more levels and you will find the It
command; the stuff in-between the Describe
and the It
was not needed in the simple test but each has a crucial role to play. So for an introductory look at this, do not worry about memorizing it; just come back to it as you learn new pieces to see where they fit. Just after the skeleton, I describe each element to help you understand how the pieces fit together.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<file-preamble> InModuleScope <module-name> { <module-preamble> Describe <test-name> { <one-time-describe-initialization> BeforeEach { <per-test-initialization> } AfterEach { <per-test-cleanup> } Context <context-description> { <one-time-context-initialization> It <test-description> { <test-code> } . . . It <test-description> { <test-code> } BeforeEach { <per-test-initialization> } AfterEach { <per-test-cleanup> } } . . . Context <context-description> { It <test-description> { <test-code> } } } . . . Describe <test-name> { Context <context-description> { It <test-description> { <test-code> } } } } |
Here is a breakdown of each component:
File Preamble
For testing a script
(.ps1), the file preamble includes dot-sourcing the source file and dot-sourcing any helper files. For testing a module
(.psd1), the file preamble should only dot-source the module; helper files must be in the module preamble.
InModuleScope (New for 3.0!)
This is a new command introduced with Pester 3.0. It allows you to unit test your private functions within a module, i.e. those functions that you have not exported for public consumption. Very handy, indeed, for getting at that “hard to reach” code so you have better test coverage. (Of course, there is the flipside of the argument that says you should only be testing what is publicly exposed so as not to make your tests too fragile…). The argument to InModuleScope
is the name of the module that you just imported in the file preamble with Import-Module
. (Regardless of whether you used Import-Module
with a module name or a file path, InModuleScope
requires just the module name.)
Module Preamble
I scratched my head for a while until I realized the need for this important component. If you are testing a module and have used InModuleScope
, then Pester creates a new scope: any initialization code before InModuleScope
is not available inside InModuleScope
. (This works just like any other script block in PowerShell.) Remember the helper functions discussed above. Those need to be here in the module preamble rather than the file preamble; otherwise, you will get an error when you attempt to use one of your helper functions saying it is not defined.
Describe
A logical container for a set of tests; this block may contain any number of Context
and It
blocks. The command-line –TestName
parameter operates on the names of Describe
blocks.
Describe Setup and Teardown (New for 3.0!)
As shown in the code skeleton, a Describe
block may contain some direct code in the one-time-describe-initialization
component. This is executed just once. But Pester 3.0 now provides the BeforeEach
setup command, which executes at the beginning of each It
block contained within the Describe
block. Similarly, the AfterEach
command provides a teardown mechanism executed at the end of each It
block.
Context
A second level logical container for a set of tests; this block may contain any number of It
blocks.
Context Setup and Teardown (New for 3.0!)
Just like the Describe
block, the Context
block may optionally contain a one-time-context-initialization
component, and it may optionally provide a BeforeEach
block for setup or an AfterEach
block for teardown. If both the Describe
and Context
blocks contain a BeforeEach
, the BeforeEach
in the Describe
executes first, then the BeforeEach
in the Context
. The AfterEach
works the other way: AfterEach
in the Context
then AfterEach
in the Describe
.
One unique feature for both BeforeEach
and AfterEach
: they will execute as just described regardless of where they physically appear within a Describe
or a Context
block. Thus, in the code skeleton, you will observe that I put them at the top of a Describe
block and at the bottom of a Context
block, just as a visual mnemonic of this.
It
A unit test. This block may appear within either a Context
block or a Describe
block.
Syntax Alert
It is a syntactic requirement of PowerShell that each opening brace you see in the code skeleton above be on the same line as the command with which it is associated. If you really prefer having your braces match by indent (and some people do, I understand!) you must use a back tick to signal line continuation, like this:
1 2 3 4 5 6 7 8 9 10 |
Describe <test-name> ` { Context <context-description> ` { It <test-description> ` { <test-code> } } } |
Every script block attached to a Pester command must start
on the same line as the command unless you escape the line break.
Pester Command Summary
For completeness, here is a list of all Pester commands showing parameters for each. Almost all of these will be touched upon in this series. For further details on any command, though, you can use PowerShell’s own Get-Help
, but typically, you will get better information from the official Pester API documentation at https://github.com/pester/Pester/wiki.
(At the time of writing, though, you will not find the syntax for the Should
command shown below anywhere else!)
InModuleScope [-ModuleName] <String> [-ScriptBlock] <ScriptBlock> |
Describe [-Name] <String> [-Tags <Object>] [[-Fixture] <ScriptBlock>] |
Context [-Name] <String> [[-Fixture] <ScriptBlock>] |
It [-name] <String> [[-test] <ScriptBlock>] |
BeforeEach <ScriptBlock> |
AfterEach <ScriptBlock> |
Mock [[-CommandName] <String>] [[-MockWith] <ScriptBlock>] [-Verifiable] |
Assert-MockCalled [[-CommandName] <String>] [-Exactly] [[-Times] <Int32>] [[-ParameterFilter] <ScriptBlock>] [[-ModuleName] <String>] [[-Scope] <String>] |
Assert-VerifiableMocks |
Should [ Not ] <Operator> [ Argument ] |
In [[-path] <Object>] [[-execute] <ScriptBlock>] |
New-Fixture [[-Path] <String>] [-Name] <String> |
Invoke-Pester [[-Path] <String>] [[-TestName] <String>] [[-EnableExit]] [[-OutputXml] <String>] [[-Tag] <String>] [-PassThru] [-CodeCoverage <Object[]>] |
Conclusion
Congratulations! If you made it this far I surmise you see the promise of unit testing in the PowerShell realm. Pester, as you saw at the very beginning, makes it quite simple and painless to unit test a function. But as with any technology you have not previously been exposed to, there is that little bit of learning curve to climb. You have surmounted the first hill by slogging through the section on test anatomy and are well on your way to conquering Pester’s secrets.
As you will see in parts 2 and 3, Pester’s learning curve is particularly modest. There you will learn about how Pester can easily validate simple data (scalars) but needs just a bit of help when dealing with arrays. Also, you will see how straightforward Pester’s mocking capability is. (Mocking allows you to make tests much simpler by focusing on the one function you want to test and faking out all the rest, even if they are built-in PowerShell cmdlets!) Another important one is how to parameterize a test. (Parameterized tests save you from writing essentially the same test over and over with just a different input value.)
By the time you are through parts 2 and 3 you will have a thorough understanding of how to use Pester’s full capabilities to make your PowerShell scripting more useful and more productive!
Load comments