Pester
Now, it's time to visit another PowerShell utility called Pester. Pester helps in defining and executing tests on PowerShell scripts, functions, and modules. These could be unit tests, integration tests, or operational validation tests. Pester is an open source utility freely downloadable from GitHub as a PowerShell module. It is also available out-of-the-box on Windows 10. We will use Pester to define unit tests and operational validation tests and also execute them in continuous integration build pipelines and continuous deployment release pipelines in this book. As of writing this book, Pester 3.0 is the latest stable version available for download and the same is used.
Pester helps in defining unit tests in a simple English style language using simple constructs like Describe
and It
and validates them through assertions. Pester can be downloaded as a ZIP file from https://github.com/pester/pester/archive/master.zip. After downloading, the ZIP file should be unblocked and its content should be extracted to a well-defined module location. We already know that modules live at both $env:windir\system32\WindowsPowershell\v1.0\modules
and $env:ProgramFiles\WindowsPowershell\modules
and Pester should be extracted to any one of these folder locations. We will use the modules folder from $env:ProgramFiles
to store the Pester module.
Installing Pester
This step is not required to be performed on Windows Server 2016 or a Windows 10 machine since the Pester module is available on them by default.
On machines with WMF 5.0, package management module should be used to download and install Pester instead of the next steps.
The code shown next should be executed on servers on which Pester is not available by default. Pester is available by default only on Windows Server 2016 and Windows 10. The code shown next is also depended on WMF 4.0.
The steps mentioned in the previous section will be automated through PowerShell for installing Pester. The entire script Install-Pester.ps1
is shown here:
Param( # folder location for storing the temp pester downloaded files [string]$tempDownloadPath ) # create a temp folder for downloading pester.zip New-Item -ItemType Directory -Path "$tempDownloadPath" -Force # download pester.zip from GitHub Invoke-WebRequest -Uri https://github.com/pester/pester/archive/master.zip -OutFile "$tempDownloadPath\pester.zip" # files from internet are generally blocked. unblocking the archive file Unblock-File -Path "$tempDownloadPath\pester.zip" # extracting files from archive file to Well-defined modules folders Expand-Archive -Path "$tempDownloadPath\pester.zip" -DestinationPath "$env:ProgramFiles\WindowsPowershell\Modules" -Force # renaming the folder from pester-master to pester Rename-Item -Path "$env:ProgramFiles\WindowsPowershell\Modules\Pester-master" -NewName "$env:ProgramFiles\WindowsPowershell\Modules\Pester" -ErrorAction Continue # test to check if pester module is available Get-Module -ListAvailable -name pester
Use the code shown here to execute the script and install Pester on a system. This code assumes that the Install-Pester.ps1
script is available at C:\temp
C:\Install-Pester.ps1 -tempDownloadPath "C:\temp"
Let me explain each line within the script.
The script starts with accepting a single parameter $tempDownloadPath
of string
type. This path should be provided by the caller as argument to the script:
Param ( # folder location for storing the temp pester downloaded files [string]$tempDownloadPath )
The next statement creates a new folder at the location provided by the user. Force
ensures that an error is not generated even if a folder with the same name exists:
New-Item -ItemType Directory -Path "$tempDownloadPath" -Force
Then, the Invoke-WebRequest
cmdlet is used to download the Pester archive file from GitHub and store the downloaded file as pester.zip
to the user's folder:
Invoke-WebRequest -Uri https://github.com/pester/pester/archive/master.zip -OutFile "$tempDownloadPath\pester.zip"
By default, downloaded ZIP files are blocked and cannot be used without unblocking them. Code shown next unblocks the downloaded pester.zip
file using Unblock-File
cmdlet.
Unblock-File -Path "$tempDownloadPath\pester.zip"
Next, the archive file is expanded and all its files and folders are extracted to the modules folder which is a well-defined module location:
Expand-Archive -Path "$tempDownloadPath\pester.zip" -DestinationPath "$env:ProgramFiles\WindowsPowershell\Modules" -Force
The extracted folder name is Pester-master
however; the name should be Pester
. The Rename-Item
cmdlet is used to rename pester-master
to pester
. This cmdlet will throw an error if the folder named Pester-master
does not exist. Normally, the script will terminate if an error occurs. With ErrorAction
as continue
, Rename-item
will throw an error but script execution will not stop:
Rename-Item -Path "$env:ProgramFiles\WindowsPowershell\Modules\Pester-master" -NewName "$env:ProgramFiles\WindowsPowershell\Modules\Pester" -ErrorAction Continue
Finally, a small test is conducted to ensure the Pester module is available using the Get-Module
cmdlet. If this cmdlet outputs the name
and version
of the pester
module, it means that Pester is successfully installed on the machine:
Get-Module -ListAvailable -name pester
Writing tests with Pester
Pester provides an easy to use New-Fixture
cmdlet. Executing this cmdlet creates two files. The first file is for authoring PowerShell scripts and functions and the second file is for authoring unit tests for code in the first file. This cmdlet also ties both the files together by generating scaffolding code in such a way that the entire script in first file is loaded into the workspace of the second file when the second file containing unit tests are executed using the concept of Dot-sourcing. The scaffolding code ensures that PowerShell scripts and functions are available to the test cases for invocation. Pester should be able to execute any code from the first file that are referred to in test cases. It is important to understand that New-Fixture
cmdlet helps in importing a script into another workspace. This can be done manually by Dot-sourcing the script files into the Pester unit tests file. In fact, New-fixture
cmdlet also dot-sources the script file into a unit tests file.
Understanding Pester is much easier by experiencing it rather than going through theory. Let's understand Pester through a scenario of writing a simple function for adding two numbers and corresponding tests for testing the same.
- Let's create a folder at
C:\
that will store script as well as tests. Let's name itAddition
. - From any PowerShell host execute the commands shown here to generate the scripting files. The
cd
command changes the directory to theC:
drive.Import-Module
cmdlet imports the Pester module into the current workspace andNew-Fixture
cmdlet from the Pester module creates the script files at theAddition
folder location. It will generate two files:Add-Numbers.ps1
andAdd-Numbers.Tests.ps1
:cd C:\ Import-Module -Name Pester New-Fixture -Path "C:\Addition" -Name "Add-Numbers"
The script generated in
Add-Numbers.ps1
will contain the logic that should be tested and an emptyAdd-Numbers
function created is shown here:function Add-Numbers { }
The script generated in
Add-Numbers.Tests.ps1
is shown here.$here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Describe "Add-Numbers" { It "does something useful" { $true | Should Be $false } }
The first three statements get the path of the
add-Numbers.ps1
script file and loads it in the current runspace using Dot-sourcing.The variable,
$here
contains the parent folder ofAdd-Numbers.Tests.ps1
inC:\Addition
.The second statement gets the file name of the tests script file as
Add- Numbers.Tests.ps1
and replaces the\.Tests\.
with a single.
then assigns it to the variable,$sut
. $sut contains theAdd-Numbers.ps1
value which is actually our script file containing application logic.The third statement simply combines both the folder path and script file name and loads it by Dot-sourcing it. This makes our
Add-Numbers
function available to the test cases defined in the tests script file.The last three statements refer to a single test case generated by
New-Fixture
cmdlet.Describe
refers to a collection of tests cases. It is a container that can contain multiple tests. It is actually a function defined in the Pester module that accepts a script block. It should reflect the component getting tested.It
refers to a single test case and its naming should indicate the nature of test performed. It is also a function defined in the Pester module that accepts a script block. This script block contains the actual tests and assertions. The assertions determine whether the test is successful or not. A successful assertion is shown in green color while failures are shown in red color.Should be
is an assertion command. There are multiple assertions provided by Pester such asShould be
,Should BeExactly
,Should match
,Should Throw
and more. - Modify the
Add-Number.ps1
script file with real code. TheAdd-Number
script looks like the following:function Add-Numbers { param ( [int] $Num1, [int] $Num2 ) return $Num1 + $Num2 }
The
Add-Numbers
function has been modified to accept two parameters,Num1
andNum2
both of integer data type. It adds both the numbers and returns back the same to the caller. - The
Add-Numbers.tests.ps1
script has been modified by removing the default test provide by Pester and adding two test cases within the sameDescribe
section. TheDescribe
section has been renamed totest cases adding two numbers
The first test named
checking when both the numbers are positive
declares two variables;$FirstNumber
and$Secondnumber
and assigns values to them. It invokes theAdd-Number
function passing both the variables as arguments to it. The return value from the function is piped and asserted (verified). Similarly, the second test namedchecking when one number is positive and another negative
again declares two variables$FirstNumber
and$Secondnumber
and assign values to them. However, this time the value of one of the variables is negative. It invokes theAdd-Number
function passing both the variables as arguments to it. The return value from the function is piped and asserted:$here = Split-Path -Parent $MyInvocation.MyCommand.Path $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.' . "$here\$sut" Describe "test cases adding two numbers" { it "checking when both the numbers are positive" { $FirstNumber = 10 $SecondNumber = 20 Add-Numbers -Num1 $FirstNumber -Num2 $SecondNumber | should be 30 } it "checking when one number is positive and another negative" { $FirstNumber = -10 $SecondNumber = 20 Add-Numbers -Num1 $FirstNumber -Num2 $SecondNumber | should be 10 } }
- Its time now to run the tests. The test can be executed by using
Invoke-Pester
cmdlet provided by the Pester module. This cmdlet takes the path of theTests
scripts file. Execute the cmdlet as shown here to execute the tests written earlier:Invoke-Pester -Script "C:\Addition\Add-Numbers.Tests.ps1"
This will invoke the scripts and execute all the test cases described in it. The same is shown in Figure 13. The green color denotes a successful test case and red one means a failed test case.
Figure 13: Executing Pester unit tests
Pester real-time example
Let's work on another example on Pester. This time we will write tests for ensuring whether a website and its related applications are in a working condition on a web server. This time New-Fixture
is not used for generating the script files. Instead, both the application code and test cases are written from scratch using a PowerShell ISE editor.
Both the code for provisioning of web server artifacts and related tests are within the same file although they can be in different files as seen before.
The entire code is shown here. The script is stored at C:\temp\Test-WebServer.ps1
. A CreateWebSite
function is defined taking four parameters. These parameters capture inputs for the application pool name ($appPoolName
), website name ($websiteName
), its port number ($port
), and the path referred by the website ($websitePath
). The function first creates an Internet Information Server (IIS) application pool first using New-WebAppPool
cmdlet and $appPoolName
parameter and then creates a IIS website using New-Website
cmdlet using all the four parameters:
function CreateWebSite { param( [string] $appPoolName, [string] $websiteName, [uint32] $port, [string] $websitePath ) New-WebAppPool -Name $appPoolName New-Website -Name $websiteName -Port $port -PhysicalPath $websitePath -ApplicationPool $appPoolName -Force } Describe "Status of web server" { BeforeAll { CreateWebSite -appPoolName "TestAppPool" -websiteName "TestWebSite" -port 9999 -websitePath "C:\InetPub\Wwwroot" } AfterAll { Remove-Website -Name "TestWebSite" Remove-WebAppPool -Name TestAppPool } context "is Website already exists with valid values" { it "checking whether the website exists" { (Get-Website -name "TestWebSite").Name | should be "TestWebSite" } it "checking if website is in running condition" { (Get-Website -name "TestWebSite").State | should be "Started" } } }
The test cases are written in the same file. A Describe
named Status of web server
is defined and represents a group of test cases. A special construct BeforeAll
and AfterAll
is used within the Describe
block. BeforeAll
runs the script within it just once for all test cases in a Describe
block before the execution of the first it
block. Similarly, the script within AfterAll
is executed after all the test cases (it
blocks) are executed. They are typically used for setting up and cleaning up the environment. Here, we invoke our CreateWebSite
function within the BeforeAll
block to provision our application pool and website. Both the application pool and website is removed in the AfterAll
block. This ensures that the environment is in the same state as before the start of the tests.
Context
is also a new container construct that can contain and group multiple test cases (it
blocks). Context does not affect the execution of tests. They remain the same as before however it adds additional metadata and groups tests based on condition. For example, a context is a group of test cases with different valid values while another context is a group of test cases with invalid values.
There are two test cases implemented. The test case checking whether the website exists
uses the Get-Website
cmdlet to get the name of the website and asserts on it. The test case, checking if website is in running condition
again uses the same cmdlet but checks its status property and compares with Started
value for assertion.
Executing the above script with Invoke-Pester
cmdlet shows the result as shown in Figure 14.
Invoke-Pester -Script "C:\temp\Test-WebServer.ps1"
Figure 14: Executing Pester tests
It is important to note the naming pattern of Describe
, Context
, and it
blocks. They have been named in such a way that the result of executing the unit tests can be read as simple English which is meaningful and provide enough context about the tests that are successful and ones that failed.