When people (at least IT people) thinks about PowerShell, they think about some magic commands and maybe about some scripts and sometime more.
PowerShell is more than that. It’s not only a sequential programming language it’s also a functional language. You can create functions and modules, but not only you can use it as configuration management with DSC and other tools, you can use it in serverless platforms like AWS Lambda or Azure Functions, you can build cloud solutions either on Azure or AWS. You can build complete solutions with PowerShell.
Even if PowerShell is seen as an Ops tools it follows the same methods and patterns than any other programming language.
Unit testing is one of these patterns. It ensures that section by section codes work as expected.
PowerShell own a Unit testing framework. Its name is Pester, it’s the ubiquitous test and mock framework for PowerShell. It’s a Domain Definition Language and a set of tools to run unit and acceptance test.
Installing Pester
Even if Pester is now installed by default on Windows 10, it’s better to update it to the latest version (now in 4.8.x, soon in 5.x).
Pester can be installed on Windows PowerShell (even on old version) and on PowerShell Core
Install-module -name Pester
You may need to use the force parameter if another is already installed
Install-module -name Pester -force
Yes, you don’t need to use -SkipPublisherChek anymore, the Pester module is signed now.
The basic
A test script starts with a Describe. Describe block create the test container where you can put data and script to perform your tests. Every variable created inside a describe block is deleted at the end of execution of the block.
Describe {
# Test Code here
}
you can add tags and a name to each describe block to define testing scenario
Describe -tag "SQL" -name "test1" {
# Test Code here
}
Describe -tag "SQL" -name "test2" {
# Test Code here
}
Tests inside a describe block can be grouped into a Context.
Context is also a container and every data or variable created inside a context block is deleted at the end of the execution of the context block
Context and Describe define a scope.
Describe -tag "SQL" -name "Sqk2017" {
# Scope Describe
Context "Context 1" {
# Test code Here
# Scope describe 1
}
Context "Context 2" {
# Test code Here
# Scope describe 2
}
}
It Block
To create a test with Pester we simply use the keyword It. The It blocks contain the test script. This script should throw an exception.
It blocks need to have an explicit description It "return the name of something" and a script block. The description must be unique in the scope (Describe or Context).
It description can be static or dynamically created (ie: you can use a variable as description as long as the description is unique)
The command Should let you to compare objects, something from your code against something expected, and throw an error if the It bloc fail.
Should introduce an assertion, something that must be true and if not throw an error.
There are several assertions:
ASSERTIONS | DESCRIPTION |
---|---|
Be | Compare the 2 objects |
BeExactly | Compare the 2 objects in case sensitive mode |
BeGreaterThan | The object must be greater than the value |
BeGreaterOrEqual | The object must be greater or equal than the value |
BeIn | test if the object is in array |
BeLessThan | The object must be less than the value |
BeLessOrEqual | The object must be less or equal than the value |
BeLike | Perform a -li comparaison |
BeLikeExactly | Perform a case sensitive -li comparaison |
BeOfType | Test the type of the value like the -is operator |
BeTrue | Check if the value is true |
BeFalse | Check if the value is false |
HaveCount | The array/collection must have the specified ammount of vallue |
Contain | The array/collection must contain the value, like the -contains operator |
Exist | test if the object exist in a psprovider (file, registry, ...) |
FileContentMatch | Regex comparaison in a text file |
FileContentMatchExactly | Case sensitive regex comparaison in a text file |
FileContentMatchMultiline | Regex comparaison in a multiline text file |
Match | RegEx Comparaison |
MatchExactly | Case sensitive RegEx Comparaison |
Throw | Check if the ScriptBlock throw an error |
BeNullOrEmpty | Checks if the values is null or an empty string |
Describe "test" {
It "true is not false" {
$true | Should -Be $true
}
}
As you can see the It block is divided into two parts. The first one, the name, identify the test. The second one, the Script bock can contain any code you need as long as you pass the result to a Should statement by Pipeline. The should Statement will evaluate the data and throw an error if the condition of the statement aren’t meet.
We can also build negative assertion with the -not keyword
Describe "test" {
It "true is never false" {
$true | Should -not -Be $false
}
}
When I write unit tests, often I need to test the same portion of code or function several times with different conditions or different data. As I am lazy, I don’t like to write the same test more than one or maybe two times.
For that I just need a dictionary object and -testcase
$BufferSize = 4
$DataTypeName = [System.Byte[]]::new($BufferSize)
$RandomSeed = [System.Random]::new()
$RandomSeed.NextBytes($DataTypeName)
$TestData = @(@{"TestValue" = "Value One"; "TestType" = "String"},@{"TestValue" = 2; "TestType" = "int32"},@{"TestValue" = $DataTypeName; "TestType" = "Byte[]"})
function get-DataTypeName {
[CmdletBinding()]
param (
$value
)
return ($value.getType()).name
}
Describe "test some values" {
It "Test if <TestValue> is a <TestType> Object" -TestCase $TestData {
param($TestValue, $TestType)
get-DataTypeName -value $TestValue | Should -Be $TestType
}
}
When you use -TestCase with a dictionary object, the It block will iterate the object and perform a test for each iteration.
To display the value in the It description the <> is needed. Inside the It bloc; you need to pass the data as parameters and get their value in parameters variables
On how to write and run Pester tests
You can virtually run pester in any PowerShell file but by convention, it’s recommended to use a file named xxx.tests.ps1. One per PowerShell file in your solution. If you have one file per function it mean one pester .tests.ps1.
You can place pester files in the same folder as your source code or you can use a tests folder at the root of the repository.
Personally, I prefer to use a tests folder but wherever the test files are, they need to load the code.
To do that you can dot source the script you need to test.
To run all the tests in the .tests.ps1 files, you just need to use invoke-pester from the tests folder.
If the It and Describe blocs are the heart of the testing process, invoke-pester can be the liver as it can filter tests and transform the result.
By using -Testname or/and -Tag parameter in conjunction with -passThru you can filter describe bloc with the name of the test or the tags.
The -Strict param allow to consider any pending or skyped test will create a fail.
By default, pester produce the result directly as a text in the console. In some situation you may want to use another format.
The -PassThru parameter let you produce a custom PowerShell object instead of standard output. It produces a PSCustomObject with the number of tests, the number of passed, skipped, pending or failed. It contains also an array, TestResult with all the test result.
The -EnableExist switch will make invoke-pester exit with an exit code. This code is 0 if all tests pass or the number of failled test. This can be usefull is some continuous integration scenarios.
OutputFile and OutputFormat parameters will redirect the output to a file in NUnit xml format. By default, Pester 4.x will output the result using NUnit format using 2.5 Schema. You cannot change the format event with -OutputFormat (there is only one possible value NUnitXml). Using the Output can be useful with some CI tool which display Nunit test result
Code Coverage
Code coverage measure the degree to which your code is executed by your tests. The measure is expressed in a percentage.
The coverage percentage don't mean that the module or the script is bug free, it only mean that the code has been run by during the test process.
To generate code coverage metric with Pester you need to use invoke-pester with -coverage following the path of the script you need to test.
invoke-pester -script .\function.test.ps1 -coverage .\function.ps1
You can use wildcard and a coma to analyze multiple files
invoke-pester -script .\functions.test.ps1 -coverage .\function.ps1, .\*.psm1
You can also use a hashtable for coverage. It must contain the Path key. You can use the Function key to limit the analyze to these functions or you can use StartLine or EndLine to limit the analyze.
Note that having a 100% coverage do not implies that the code is bug free, it is just an indication that all your code has been executed during the test
Advanced usage
Test Drive
A function may need to manipulate the file system, it can create, delete or modify one or more files. It isn't desirable to change file system during the test phase.
Pester provide a drive named TestDrive:. The drive is available in the scope of the test (Describe or Context) and automatically deleted at the end of the scope.
When using the test drive, Pester create a random folder in $env:tmp and use it to put file from the current test drive in the scope.
You can use testdrive:\ or the $testdrive variable to perform any file operation during the test.
Describe "test" {
new-item (Join-Path $TestDrive 'File.txt')
It "Test if File.txt exist" {
(test-path -path (Join-Path $TestDrive 'File.txt') ) | Should -Be $true
}
}
Mocking
Scripts and functions rely on modules and function that may not be present on the test machine, or perform a destructive operation.
You design a function that create or delete an active directory account.
you may not have any active directory module on the test computer and/or you do not want to make any change on production computer
You create a module that change the configuration of the server and you don't want to change anything during the test phase.
You just need to be sure that the code is correct.
Mocking in Pester, let you imitate the result of a function or command called during the test. When you use Mock, you simply create the result for a given command so you can test other part of your script.
To mock a function or a command you simply need to use Mock and the name of the function or the command.
Describe "test" {
Mock remove-something { }
It "Test if remove-something return null" {
remove-something | Should BeNullOrEmpty
}
}
Mocking is limited by the scope of the test, either Describe or Context. If you need to test different behavior you can use -ParameterFilter to create different result
Describe "test" {
Mock Get-AswerAboutLifeUniverseEverything { return 42 } -ParameterFilter { $Name -eq "42" }
Mock Get-AswerAboutLifeUniverseEverything { return 42 } -ParameterFilter { $Name -eq "57" }
Mock Get-AswerAboutLifeUniverseEverything { return 42 }
It "Test if Get-AswerAboutLifeUniverseEverything return 42" {
Get-AswerAboutLifeUniverseEverything | Should be 42
}
It "Test if Get-AswerAboutLifeUniverseEverything -Name 42 return 42" {
Get-AswerAboutLifeUniverseEverything -Name 42 | Should be 42
}
It "Test if Get-AswerAboutLifeUniverseEverything -Name 57 return 42" {
Get-AswerAboutLifeUniverseEverything -Name 57 | Should be 42
}
}
You can use Mock inside an IT block, in this case Mock will be available in the parent scope.
You may want to test if a mocked command was called during the test phase. It's possible to test this in 2 ways
Assert-VerifiableMocks, throw an error if a Mock command marked as -verifiable
Describe "test" {
Mock Get-AswerAboutLifeUniverseEverything { return 42 } -Verifiable
It "Test if remove-something return 42" {
Get-AswerAboutLifeUniverseEverything | Should be 42
}
Assert-VerifiableMocks
}
Another way to test the execution of a mocked command is to use Assert-MockCalled.
Assert-MockCalled must be placed inside an IT block. You need to use it with the -CommandName parameter. The Parameter take the name of the mocked command
Describe "test" {
Mock Get-AswerAboutLifeUniverseEverything { return 42 }
It "Test if remove-something return 42" {
Get-AswerAboutLifeUniverseEverything | Should be 42
}
It "Test if remove-something was called" {
Assert-MockCalled -CommandName Get-AswerAboutLifeUniverseEverything
}
}
You can control how many times the mocked command was called with -times parameter. The Times X parameter test if the mocked command was called at least X times.
If you need to test that the mocked command is called only X times you can use -Exactly
When mocking a function or a cmdlet, you may need to return an object. For example Get-AdUser return a Microsoft.ActiveDirectory.Management.ADUser object. Most of the time you can use a PScustom object to mock the returned object
Describe "test" {
Mock Get-AdUser {
$AObject = [PSCustomObject]@{
Surname = "Marc"
UserPrincipalName = "marc@mydomain.com"
Enabled = $true
SamAccountName = "marc"
ObjectClass = "user"
}
Return $AdObject
} -ParameterFilter { $Identity -eq "Marc" }
It "Test if the User is Valid" {
(Get-AdUser -identity “Marc”).Enabled | Should betrue
}
It " Test if the User UPN " {
(Get-AdUser -identity “Marc”).UserPrincipalName | Should be “marc@mydomain.com”
}
}
But sometime a PsCustomObject is not enough. This is the case when you use class in your script.
You can use New-MockObject to return any type of object. New-MockObject fake any type of object without the need to create it. You can use it to mock any .Net Type or you own class-based type
Mock Get-Something {
New-MockObject -type 'System.Diagnostics.Process'
}
In some situation, you may need to execute some code before of after the tests. It could be variable initialization or environment setup.
For example, you can create some files inside a test drive, open a connection to webservice, create random data.
BeforeAll and AfterAll must reside inside a Describe or Context Block. It can be placed anywhere inside the block but it's better to place BeforeAll at the beginning and after all at the end.
BeforeEach and AfterEach run on each IT Block in the same way as BeforeAll/AfterAll
Module Testing
Pester can be used to test PowerShell module. But remember, pester need to access to the code to test code.
First step, as module are loaded in memory you need to instruct PowerShell to remove the module. If not, you may not test the actual version of your code.
Get-module -name ‘TheModule’ -all | remove-module -force -erroraction SilentContinue
You can import the module
Import-Module -name PathTo\TheModule.psd1 -force -ErrorAction Stop
Problems, doing so you will only have access to public function declared inside FunctionToExport.
To avoid that, you need to use InModuleScope
InModuleScope TheModule {
Describe 'Module Test' {
It 'test one not exported function' {
Get-NotExportFunction | Should not Throw
}
}
}
This was my introduction to Pester. Tell me what you thinks
Top comments (6)
First of all, thanks you Olivier for this AWESOME introduction to Pester!
I'm trying to mock a function but isn't working as expected. The mocking is:
Mock Get-PnPList{return @{"Title"="testList"}} -ParameterFilter { $Identity -eq "testList" }
Mock Get-PnPList{return $null } -ParameterFilter { $Identity -ne "testList" }
The script code is:
function checkListExists{
Param([Parameter(Mandatory = $true)][string] $listTitle)
$list = Get-PnPList -Identity $listTitle
if($null -eq $list){
return $false
}
else{
return $true
}
}
And finally I'm calling
checkListExists "testList" | Should -BeTrue
But the test fails: "Expected $true, but got $false."
I'm debugging and the parameter -Identity $listTitle of the Get-PnPList function is receiving the value "testList".
Why the moking is not working as expected?
Thanks!
I plan to write a post about mocking in Pester
You should try to do somehitng like that
Mock Get-PnPList -MockWith {
[pscustomobject]@{
"Title" = "testList"
}
} -ParameterFilter { $Identity -eq "testList" }
Hi, thanks for the answer. Tried like you said but doesn't work. Finally I have found that the problem was the -ParameterFilter. Doing $Identity.toString() -eq "testList" it works perfectly. That .toString() makes the difference!
Hi Olivier,
Thanks for the post. I do still have a scenario that I can't get to work.
The test is in another file:
When running this test, I get the error that my Azure credentials have not been set, indicating the function call in the other function was not mocked. Hence, the scoping solution does not seem to have the desired effect.
Do you know how to solve this?
Solved!
The issue seemed to be session related. When I define the
Get-AzResource
function in theBeforeAll
, it works like a charm.Alternatively you may try Tomtit for simple yet efficient blackbox testing of Powershell code.