DevOps with Windows Server 2016
上QQ阅读APP看书,第一时间看更新

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.

  1. Let's create a folder at C:\ that will store script as well as tests. Let's name it Addition.
  2. From any PowerShell host execute the commands shown here to generate the scripting files. The cd command changes the directory to the C: drive. Import-Module cmdlet imports the Pester module into the current workspace and New-Fixture cmdlet from the Pester module creates the script files at the Addition folder location. It will generate two files: Add-Numbers.ps1 and Add-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 empty Add-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 of Add-Numbers.Tests.ps1 in C:\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 the Add-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 as Should be, Should BeExactly, Should match, Should Throw and more.

  3. Modify the Add-Number.ps1 script file with real code. The Add-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 and Num2 both of integer data type. It adds both the numbers and returns back the same to the caller.

  4. The Add-Numbers.tests.ps1 script has been modified by removing the default test provide by Pester and adding two test cases within the same Describe section. The Describe section has been renamed to test 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 the Add-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 named checking 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 the Add-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 
                  } 
             } 
    
  5. 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 the Tests 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.