To perform a complete Pester test of PowerShell code requires a few different “layers.” These layers test various functional aspects of the code. In this article, I’ll be discussing two of those layers: unit tests and integration tests.
Avatar

When considering “infrastructure code” or the code that automates infrastructure components like PowerShell in the Windows world, the general purpose is to change the state of some environmental components, like a virtual machine, a file, a registry key, a certificate, etc.

If you’d like to confirm that your code actually changed something in the environment, you’d create an integration test. An integration test actually goes out and reads whatever item your code was supposed to have changed and compares that value against an expected value. If your code was meant to create a VM, an integration test actually executes your code and then immediately confirms that the VM was created in the correct manner.

Getting more granular is a unit test, which confirms correct code execution. It is not aware of the environment at all. A unit test confirms that the code ran as you expected it to run. Whether it actually changed anything in the environment is up to the integration test. A unit test confirms your code attempted to create that file in an expected path, that VM with a particular name, or that local user account with a certain description. Unit tests ensure the code you expect to run executes and follows the pattern you intend it to.

It hasn’t been until recently that system administrators and DevOps professionals have needed to define these types of tests in traditional scripting languages. But nowadays, some of this scripting code has become just as important as the production code it supports. This has led to lots of newer testing frameworks. In the Windows ecosystem, that scripting language testing framework is Pester.

Pester is a unit testing framework built in PowerShell. It was designed to create unit tests. But as you’ll see in a minute, because it was built in PowerShell and is open source, a savvy tester is equally able to create integration tests with this framework as well.

To demonstrate unit tests versus integration tests, let’s take a simple scenario and script, define, and develop a set of unit tests and integration tests for it.

Perhaps you have a simple script called New-UserProvision.ps1 that creates an AD user account and a home folder at the same time. Your script might look something like this:

param(
     [Parameter()]
     [string]$Username,
     [Parameter()]
     [string]$OUPath,
     [Parameter()]
     [string]$HomeFolderPath
 )
 New-AdUser –Name $Username –Path $OUPath
 New-Item –Path $HomeFolderPath –ItemType Directory

Unit tests

A unit test tests code flow and intention, not what the code actually changes. Pester has a feature called mocking that helps do this. Mocking is a way of replacing what a command would do with what you’d like it to do during a test. For example, in our situation, we don’t want New-AdUser to actually attempt to create that user account. This is a unit test, not an integration test. We need to mock that command so it essentially does nothing but still allows us to find out if it was called correct. The same goes for New-Item.

To find out if the commands were called as we intended, we need to assert that they were called in the correct manner. Pester has a handy function called Assert-MockCalled that we can use to handle this.

A set of unit tests for this code might look something like this. We’ll fill in the actual assertion code in a minute. Notice that I start each test with attempts to do something. This is optional, but I like to use this word in unit tests because it doesn’t actually test if it actually did that thing.

describe ‘AD user provisioning script’ {
     it ‘attempts to create the AD user with the right name and in the right OU’ {
         .\New-UserProvision.ps1 –UserName ‘someuser’ –OUPath ‘somepath’ –HomeFolderPath ‘somefolderpath’
     }
     it ‘attempts to create the home folder at the right path’ {
         .\New-UserProvision.ps1 –UserName ‘someuser’ –OUPath ‘somepath’ –HomeFolderPath ‘somefolderpath’
     }
 }

Notice that I’ve provided bogus parameter values. That’s because it doesn’t matter what they are since we’re not actually doing anything. We just need to ensure those values can be asserted when the time comes.

Let’s now confirm the code inside our script was called correctly. I’ll do this by adding two mocks, one for each command, and then asserting that each was called correctly with the appropriate parameter values.

describe ‘AD user provisioning script’ {
     
     mock ‘New-AdUser’ {
         return $null
     }
     
     mock ‘New-Item’ {
         return $null
     }
     
     it ‘attempts to create the AD user with the right name and in the right OU’ {
         C:\New-UserProvision.ps1 –UserName ‘someuser’ –OUPath ‘somepath’ –HomeFolderPath ‘somefolderpath’
         
         Assert-MockCalled ‘New-AdUser’ –ParameterFilter { $Name –eq ‘someuser’ –and $Path -eq ‘somepath’ }
     }
     
     it ‘attempts to create the home folder at the right path’ {
         C:\New-UserProvision.ps1 –UserName ‘someuser’ –OUPath ‘somepath’ –HomeFolderPath ‘somefolderpath’
         
         Assert-MockCalled ‘New-Item’ –ParameterFilter { $Path –eq ‘somefolderpath’ }
     }
 }

When I run Pester, you can see that my unit tests are successful.

Successful unit tests

Successful unit tests

Integration Tests

Integration tests actually test if the script created the AD user and the home folder in the right spot. It will never involve mocking because we actually want the code to change the environment so we can then turn around and read the values to compare against the expected ones.

Here I’ll move the code execution of New-UserProvision.ps1 up a level right under the describe block instead of calling it twice. This would be redundant because we need to create only the AD user and home folder once but perform a couple of different tests around that.

describe ‘AD user provisioning script’ {
     
     $parameters = @{
         ‘Username’ = ‘someuser’
         ‘OUPath’ = ‘OU=MyUsers,DC=lab,DC=local’
         ‘HomeFolderPath’ = ‘\\fileserver\HomeFolders\someuser’
     }
     
     C:\New-UserProvision.ps1 @parameters
     
     it ‘attempts to create the AD user with the right name and in the right OU’ {
         
         Get-AdUser -Filter { $DistinguishedName -eq ‘CN=someuser,OU=MyUsers,DC=lab,DC=local’} | should not be $null
         
     }
     
     it ‘attempts to create the home folder at the right path’ {
         
         Test-Path -Path $parameters.HomeFolderPath | should be $true
     }
 }

Also, notice that all the mocking code is gone. For the integration test, I’m reading the values rather than writing them to confirm the changes happened. When I run this, my Pester test should again come back all green.

Summary

To test your code properly, it’s important that you incorporate both unit tests and integration tests. Unit tests will provide you with quick feedback and will make any coding errors jump out at you. Once the code is done and tested, integration tests will ensure the environment in which you’re testing against will support this, which will tell you if your code needs to be modified as a result.

Subscribe to 4sysops newsletter!

For a full breakdown on what's possible with the PowerShell testing framework Pester check out The Pester Book by myself and Don Jones.

2 Comments
  1. Avatar
    Maurice 7 years ago

    Hi Adam,

    I have a hard time figuring out a good way for handling environmental dependencies in my unit tests.

    Lets say I have a module for reading/writing a configuration file to disk. My funtion ‘Get-Configuration’ internally calls ‘Get-Content’ and ‘ConvertFrom-Json’. To test the flow and intention of the function, I mocked both ‘Get-Content’ and ‘ConvertFrom-Json’ and used ‘Assert-MockCalled’ to verify that both functions get called as expected.

    But now my unit tests are tightly coupled to the implementation of the function. If I want to change the function to use ‘[System.IO.File]::ReadAllText()’ instead of ‘Get-Content’ I have modify my unit tests, even though the functionality of the function did not change.

    Is there a good way to use unit tests in such scenarios?

    • Avatar Author

      You bring up a good point however I’m not sure if there’s a good solution. If you’re testing the execution of a function, it’s going to be hard to not couple your unit tests with the code itself. When I’m writing unit tests, I always have some execution tests but the majority of my testing is done to test the output. That way it doesn’t matter what happens inside of the function, as long as I’m getting the desired output I’m OK with that.

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account