Test your PowerShell code with Pester
Chris Noring
Posted on October 11, 2021
TLDR; this article covers the testing framework Pester that you use to test your PowerShell scripts.
Why test
The reason you want to have tests are many:
- Correctness. Ensure your code works as as intended for certain scenarios.
- Confidence. When you have a lot of tests covering your code it creates a level of confidence. With this confidence you start daring to change this, if you for example would need to refactor code and ensure it still works after those changes.
- Architecture. Another reason for having tests is that it drives architecture. If you create tests around what you do, you ensure you build your code in a way that makes it testable. That, drives the architecture.
There are many other reasons for wanting to have tests but the three above are quite compelling.
What is Pester
Pester is a test framework meant for PowerShell and is a module you can install. It has several features:
- Assertions. Pester comes with diverse ways of asserting conditions that will determine if your tests should fail or not.
- Able to run tests. You can run tests with Pester, both a single test with a single piece of input as well as testing many different inputs at once.
- Can group your tests in test suites. When you start having quite a few tests, you want a way to group those tests into larger logical groups, that's what test suites are.
- Ability to mock calls. In you tests you might have calls to commands that carry out side-effects, like accessing a data store or creating a file for example. When you want your tests to focus on the behavior on the tests, mocking is a good idea.
Install
To install Pester, you run the below command.
Install-Module -Name Pester -Force
Once it's installed, you can start authoring your tests.
From install page:
Pester runs on Windows, Linux, MacOS and anywhere else thanks to PowerShell. It is compatible with Windows PowerShell 3, 4, 5, 6 and 7.
Pester 3 comes pre-installed with Windows 10
Our first test
For our first test, we will learn how to write a test as well as running it.
- To create our first test, create a file A-Test.ps1
- Add the following code:
Describe "A suite" {
It "my first test" {
$Value = "Value"
$Value | Should -Be "Value"
}
}
The test above, have a Describe
construct which is the declaration of a suite, and a string argument, giving the suite a name. Within the suite there's a test definition It
, which also has a string argument that represents the name of the test. Within the test, there's test itself where the code is set up:
$Value = "Value"
and then it's asserted upon:
$Value | Should -Be "Value"
Note the use of the Should -Be
, which determines equality between $Value
and "Value".
- To run the test, call
Invoke-Pester
(./ for the path in Linux ans macOS and .\ for Windows):
Invoke-Pester ./A-Test.ps1
The outcome of running the test is:
Starting discovery in 1 files.
Discovery found 1 tests in 6ms.
Running tests.
[+] /<path>/A-Test.ps1 42ms (11ms|26ms)
Tests completed in 44ms
Tests Passed: 1, Failed: 0, Skipped: 0 NotRun: 0
A more real looking test
The first test was great in that it let us understand the mechanics of testing and concepts like a suite, a test, and an assertion like Should -Be
. A more real looking test would test code in a script file that's not in our test file. Here's the steps we will take next:
- Create a script file with our production code.
- Dot source said script file.
- Create a test and run it.
Create production code
You will have code that you want to test but let's create this script file for the sake of demonstration.
- Create a file Get-Tomato.ps1
- Add the following code:
Function Get-Tomato() {
new-object psobject -property @{ Name = "Tomato" }
}
This code will create a custom object for each time the Get-Tomato()
function is invoked.
- Let's dot source it next so are session knows about it:
. ./Get-Tomato.ps1
- Verify that your function has been picked up by running:
Get-Tomato
you should see the following in the console:
Name ----
Tomato
Create the test
Now that we have our production code, let's author the test next.
- Create a file Get-Tomato.Tests.ps1 and give it the following code:
Describe "Tomatoes" {
It "Get Tomato" {
$tomato = Get-Tomato
$tomato.Name | Should -Be "Tomato"
}
}
- Run the test with
Invoke-Pester
:
Invoke-Pester ./Get-Tomato.Tests.ps1
you should see the following output:
Invoke-Pester ./Get-Tomato.Tests.ps1
Starting discovery in 1 files. Discovery found 1 tests in 6ms.
Running tests.
[+] /<path>/Get-Tomato.Tests.ps1 66ms (9ms|52ms)
Tests completed in 68ms
Tests Passed: 1, Failed: 0, Skipped: 0 NotRun: 0
Great, you ran a test on a more real looking code.
Working with side effects
You will have code that you write that eventually performs side effects, like accessing a network resource or create a file. Let's look at such a case and how Pester handles it. The short answer is that you can use mocks, a construct that's executed instead of the actual command. All you need to do is to focus that the right behavior happens.
Update production code
So, in this case, imagine that our production code now will have more function Save-Tomato
that saves an object to a file.
- Update the _Get-Tomato.ps1_file with this code:
Function Save-Tomato() {
Param(
[string] $Name
)
New-Item -ItemType File -Path ./Tomato.txt -Value $Name -Force
}
- Dot source the code to ensure it's being picked up:
. /Get-Tomato.ps1
Create a test
Ok, so you have added Save-Tomato()
to your script file. Now for a test. Your code is calling New-Item
, which creates a new file. As part of testing, you don't want it to create a file each time the test is being run. More likely, you just want to see the test does what it's supposed to, i.e. calling the correct command/s. So, to solve this issue, we can mock, replace the current implementation of New-Item
with our own.
-
To mock, we first need to replace the actual implementation like so:
Mock -CommandName New-Item -MockWith {}
Call the command. At this point, you need to call the command like you would usually do. In your case, it means that you call
Save-Tomato()
:
Save-Tomato # this should call New-Item
- Verify. The last part of the mocking process is to verify that you mock has been called
- The command
Should -Invoke
, allows you to specify what command it should have called, like so:
Should -Invoke -CommandName New-Item -Times 1 -Exactly
The above code verifies New-Item
is called exactly one time.
- Let's put it all together as a test:
It "Save tomato" {
Mock -CommandName New-Item -MockWith {}
Save-Tomato "my tomato"
Should -Invoke -CommandName New-Item -Times 1 -Exactly
}
- Remove Tomato.txt and then run the test with
Invoke-Pester
like so:
Invoke-Pester ./Get-Tomato.Tests.ps1
At this point your tests should run successfully like so:
Starting discovery in 1 files.
Discovery found 2 tests in 8ms.
Running tests.
[+] /Users/chnoring/Documents/dev/projects/powershell-projects/articles/Get-Tomato.Tests.ps1 78ms (34ms|37ms)
Tests completed in 80ms
Tests Passed: 2, Failed: 0, Skipped: 0 NotRun: 0
Also, note how Tomato.txt isn't created, because you are mocking the call to New-Item
, success.
Congrats, you've learned how to install test framework Pester, on top of that, you've learned to author your first tests and even learned how to mock the call to actual commands. To learn more, have a look at Pesters GitHub page:
Posted on October 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.