I’ve been using Pester for a long time off and on. I’ve always been obsessed with ensuring reliability in my PowerShell code. After writing the Pester Book and mentioning some of the methodologies I used with Pester v4 I’ll present in this blog post, I’ve since learned Pester v5 makes my work so much easier.
You see, when you build tests with Pester, your options are limitless like anything in PowerShell. You can structure tests a million different ways because you’re free to write whatever PowerShell you want inside of the tests. But that’s not always a good thing. You need some kind of methodology or general way you build them. This is my preferred way; data-driven tests.
What are Data-Driven Tests?
In a typical Pester test suite, you typically create an it
block for each assertion you need to do. You create an it
block to:
- assert the script returns the right output
- assert the script calls a correct function
- assert the script creates a file at the correct location.
- …the possibilities are endless
For each of these scenarios, you create an it
block with a name and provide all of the parameters to pass to the script to perform the action you’re testing.
For example:
describe 'some script' {
it 'creates a thing given the parameters to do so' {
./thing.ps1 -Create -Name 'xxx' | should -BeTrue
}
it 'sets a thing given the parameters to do so' {
./thing.ps1 -Set -Name 'xxx' | should -BeTrue
}
it 'removes a thing given the parameters to do so' {
./thing.ps1 -Remove -Name 'xxx' | should -BeTrue
}
}
Fair enough. You’ve defined each scenario, providing the parameters necessary and are asserting the script returns true
. This is a common way to create tests but this approach always bothered me. It felt like I was duplicating code unnecessarily when I could store stuff in an array.
It turns out, I could!
In Pester v5, you can create tests that are essentially part of foreach loop passing sets of parameters all at once to blocks and having Pester execute the test for each parameter set.
In the example above, you’re using three different parameter sets for the same script. Those sets can be defined in an array of hashtables.
$paramSets = @(
@{
Create = $true
Name = 'xxx'
}
{
Set = $true
Name = 'xxx'
}
{
Remove = $true
Name = 'xxx'
}
)
Once they’re all stored in that array, you can then use Pester v5’s foreach
feature to “attach” them to a block like an it
block.
it 'whatever test name here' -ForEach $paramSets {
## The parameter set is now represented as the current iteraction in the foreach loop
$paramSet = $_
& "./thing.ps1" @paramSet | should -BeTrue
}
When you execute the Pester test now, it will pass each parameter set in the array to the it
block executing three tests by just defining a single it
block! Much more efficient.
Keeping PowerShell Test Code DRY
Every software developer knows the concept the the DRY method. In a nutshell, it’s simply not repeating yourself. It means not duplicating code as much as possible. Why? For maintainability.
Imagine needing to create 100 database records and you see code like this:
New-Record -Table 'xxxx' -Fields @{ name = 'whatever1' }
New-Record -Table 'xxxx' -Fields @{ name = ' whatever2' }
New-Record -Table 'xxxx' -Fields @{ name = ' whatever3' }
New-Record -Table 'xxxx' -Fields @{ name = ' whatever4' }
New-Record -Table 'xxxx' -Fields @{ name = ' whatever5' }
New-Record -Table 'xxxx' -Fields @{ name = ' whatever6' }
## to 100
That’s 100 lines of code that could easily be trimmed down to four simplifying the code and making it much more maintainable.
$whateverCount = 100
for ($i = 1; $i -lt $whateverCount; $i++) {
New-Record -Table 'xxxx' -Fields @{ name = "whatever$i" }
}
Not only did it simplify the code, you can create a million records by simply changing a number. This is DRY.
Then why are some people writing Pester tests like this?
describe 'some script' {
it 'creates a thing given the parameters to do so' {
./thing.ps1 -Create -Name 'xxx'
}
it 'sets a thing given the parameters to do so' {
./thing.ps1 -Set -Name 'xxx'
}
it 'removes a thing given the parameters to do so' {
./thing.ps1 -Remove -Name 'xxx'
}
}
If you’re new to testing or really even read Pester’s documentation, you’ll see that many of the examples are structured like this. And there’s absolutely nothing wrong with that! If you’re building a small handful of tests for simple scripts, it works just fine. But it always bothered me.
Although not as obvious, you’re essentially doing the same thing; duplicating functionality. But making Pester tests DRY isn’t quite as simple as introducing a for
loop and adding a single variable. You must structure them in a way using Pester’s domain-specific language (DSL).
Better Test Coverage
Writing tests, like PowerShell code, is truly an art. There are a lot of ways to perform the same action each with benefits and drawbacks. When writing tests for large PowerShell scripts, you can go crazy and test every…little…minute…detail. Or, you can go the agile route and create a small set of tests that cover the majority of situations and just add tests as you discover how the script fails when it’s being used in the wild.
How do you strike up a compromise between the two? Data-driven tests!
With data-driven tests, you can define every, single way your script can be called by defining every parameter set possible in an array ahead of time.
How to do Do Pester’s Data-Driven Tests
Now that you’ve been acquainted to data driven tests, let’s now cover a methodolody I personally use to ensure manageable test coverage for my PowerShell code.
- Define the mandatory parameters in a hashtable.
$mandatoryParameters = @{ EndpointApiKey = (ConvertTo-SecureString -String 'apikeyhere' -AsPlainText -Force) PasswordName = 'namehere' NewPassword = (ConvertTo-SecureString -String 'passwordhere' -AsPlainText -Force) }
If you have mandatory parameters, you know you’ll have to pass these to the script for every run so define them first.
- Create an array of hashtables including both parameter set hashtable and the test name.
$mandatoryParameters = @{ EndpointApiKey = (ConvertTo-SecureString -String 'apikeyhere' -AsPlainText -Force) PasswordName = 'namehere' NewPassword = (ConvertTo-SecureString -String 'passwordhere' -AsPlainText -Force) } $parameterSets = @( @{ label = 'all mandatory parameters' parameter_set = $mandatoryParameters } @{ label = 'specific EndpointUri' parameter_set = $mandatoryParameters + @{ 'EndpointUri' = 'https://endpointhere' } } )
Notice that instead of replicating the mandatory parameters, I simply add more parameters to the parameter set in the
parameter_set
key. - Create the main
context
block that will apply to all situations.context 'Global' { ## do whatever in here that depends on the script }
- Create a
context
block for each environmental situation.context 'when the file exists' { } context 'when the file does not exist' { }
- Limit the parameter sets passed to each context by filtering them out beforehand.
context 'when the file exists' { $ctxParameterSets = $parameterSets.where({ $_.parameter_set.ContainsKey('EndpointUri'') }) }
- Create
it
blocks representing whatever tests you need to do.it 'passes the expected URI to the API to update the password : <_.label>' -ForEach $parameterSets { & "$PSScriptRootthing.ps1" @parameter_set | should.... }
In Pester v5, you can insert a string inside of the test name. I use a key from the array defined first called label to inject a description of what that parameter set is.
Once I have all of the tests built like this, I can then simply add parameter sets to the array which will automatically run all of the tests I need!