Practical PowerShell Unit-Testing: Getting Started

By the time you're using PowerShell to automate an increasing amount of the system administration, database maintenance, or application-lifecycle work, you'll probably need to automate the unit-testing of the PowerShell scripts themselves. Michael Sorens introduces you to Pester, the leading test framework for PowerShell, and shows how it can make it easier to produce reliable scripts

Contents

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 –

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:

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:

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:

Let’s change it to simply return a phrase:

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

Notice also that I changed the original dummy test expect $true, so both tests should pass when you execute invoke-pester:

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:

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:

Running Pester confirms success:

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:

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.
(2)
Specify a path and Invoke-Pester examines the tree rooted at that specified location.
(3)
Run one or more specific tests by naming them (wildcards allowed). The TestName parameter corresponds to Pester’s Describe block, which is a container for a logical group of tests. Thus, this will run tests whose Describe blocks are named GetFilesMany, GetFiles, or GetFilesLackingProjectName, for example:
2084-pencil.jpg

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!

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.

2084-pencil.jpg

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:

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:

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:

2084-pencil.jpg

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:

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.

 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:

2084-pencil.jpg

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]
     [[-ParameterFilter] <ScriptBlock>] [[-ModuleName] <String>]

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!

Tags: , ,

  • 33113 views

  • Rate
    [Total: 6    Average: 4.3/5]
  • JamesWoolfenden

    Installing Pester
    Isn’t it install-module pester not psget pester? psget does nothing.

  • ax4413

    PSGet
    James

    PSGet is a module to install powershell modules. Think NPM bower. http://psget.net/

  • JamesWoolfenden

    incorrect method call to psget
    I know that thanks. If you get the latest psget like you state, import the module …and then try "typing PSGET Pester" you get nothing. I have the module loaded fine.
    The correct command see-http://psget.net/ is install-module unless of course you’ve added an alias for install-module as psget (I got Pester fine)

  • msorens

    Re: Installing Pester/PsGet
    Straightened up the detailsabout PsGet install–thanks to those who pointed it out!

  • Dzejms

    Fantastic article
    I’m only half-way through this first post, but it’s been so helpful, I created an account just to tell you thanks!

  • Peter Taylor

    A question: when you already installed Pester. Isn’t there a way to verify you have correctly installed Pester?

  • Peter Taylor

    Need to let you know Pester has changed. The format for -OutputXml returned an error. “WARNING: The -OutputXml parameter has been deprecated; please use the new -OutputFile and -OutputFormat parameters instead. To get the same type of export that the -OutputXml parameter currently provides, use an -OutputFormat of “LegacyNUnitXml”.”