Using Pester for Test Driven Development in PowerShell

How do you normally write your PowerShell code? Your answer is probably like mine and most other IT pros. You have a task that you need to accomplish and instead of performing a clicker-size in the GUI whenever that task is required to be performed, you write a PowerShell one-liner, script, or function to automate that task. You test it a little to make sure there aren't any major issues and you implement it, moving on to the next task.

Have you heard of a PowerShell module named Pester? I'd heard of Pester in the past but never really got past the developer oriented terms that were associated with it. I was missing out big time and didn't know it and you might be too.

Pester is a PowerShell module designed for performing unit testing on your PowerShell code. Unit testing? What? I can hear most of you now saying "I'm not a developer". Well, neither am I which is why I added a link to an article on Wikipedia about that subject.

While we're on the subject of not being a developer, I've started using the phrase "writing PowerShell code" to refer to writing PowerShell one-liners, scripts, functions, etc. That's what you're writing so get over it IT pros because you can either learn to write some PowerShell code or find a new profession.

While I'm throwing out those nasty developer oriented terms, here's another one: TDD (Test-driven development). Once again, I've linked this foreign term to an article on Wikipedia on the subject for us IT pros. Go read those articles .

Here's the thought process: Instead of sitting down and writing code to perform some task or resolve a problem, why not write down what your trying to accomplish in the form of tests before jumping in head first and writing code to fix the problem you think you have (you might actually create more problems otherwise). Make sure all of those tests fail and then write PowerShell code to accomplish the necessary tasks until all of the tests pass.

Sounds interesting right? Today is a new day for us IT pros .

First, start out by downloading the Pester PowerShell module from GitHub:

1Invoke-WebRequest -Uri 'https://github.com/pester/Pester/archive/master.zip' -OutFile "$env:userprofile\Downloads\pester.zip" -Verbose

pester1.jpg

Unblock the downloaded zip file:

1Unblock-File -Path "$env:userprofile\Downloads\pester.zip" -Verbose

pester2.jpg

Extract the files contained in the downloaded zip file to a path in your $env:PSModulePath:

1Expand-Archive -Path "$env:userprofile\Downloads\pester.zip" -DestinationPath "$env:ProgramFiles\WindowsPowershell\Modules" -Verbose

pester3.jpg

Rename the "Pester-master" folder to "Pester":

1Rename-Item -Path "$env:ProgramFiles\WindowsPowershell\Modules\Pester-master" -NewName "$env:ProgramFiles\WindowsPowershell\Modules\Pester" -Verbose

pester4.jpg

Import the Pester module:

pester5.jpg

Create a test folder for Pester:

1New-Item -Path C:\Pester -ItemType Directory -Verbose

pester6.jpg

We're ready to begin on our first test-driven development task. You've been asked to create a PowerShell function to determine the parity (odd or even) of one or more numbers.

When starting on a new project, task, etc, the first step is to use the New-Fixture function from the Pester module which creates a folder specified via the Path parameter and two .ps1 files. The .ps1 file is for your PowerShell code and the test.ps1 file is where the tests go:

1New-Fixture -Name Get-NumberParity -Path C:\Pester\NumberParity -Verbose

pester7b.jpg

Change to the directory created in the previous step:

1Set-Location -Path C:\Pester\NumberParity

pester8.jpg

The Invoke-Pester function runs the tests:

1Invoke-Pester

pester9.jpg

Open both of the files from the NumberParity folder in the PowerShell ISE.

Currently the Get-NumberParity.ps1 file contains a function that doesn't do anything:

1function Get-NumberParity {
2
3}

The Get-NumberParity.Tests.ps1 file contains a test that will always fail (which is why all of the test failed when we previous ran the Invoke-Pester function):

1$here = Split-Path -Parent $MyInvocation.MyCommand.Path
2$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
3. "$here\$sut"
4
5Describe "Get-NumberParity" {
6    It "does something useful" {
7        $true | Should Be $false
8    }
9}

Now to define the specifications for what our code is suppose to accomplish in the form of tests. I'll keep it simple in this blog article and only use the Should Be assertion:

 1$here = Split-Path -Parent $MyInvocation.MyCommand.Path
 2$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
 3. "$here\$sut"
 4
 5Describe "Get-NumberParity" {
 6    It "Should determine 1 is Odd" {
 7        Get-NumberParity -Number 1 | Should Be Odd
 8    }
 9    It "Should determine 2 is Even" {
10        Get-NumberParity -Number 2 | Should Be Even
11    }
12    It "Should determine -1 is Odd" {
13        Get-NumberParity -Number -1 | Should Be Odd
14    }
15    It "Should accept more than one number via parameter input" {
16        (Get-NumberParity -Number 1,2,3).Length | Should Be 3
17    }
18    It "Should accept more than one number via pipeline input" {
19        (1..5 | Get-NumberParity).Length | Should Be 5
20    }
21}

Run our test:

pester10.jpg

Notice that all of our tests failed. At this step in the process, "failure = success" because we haven't written any code yet, at least not for the actual Get-NumberParity function and all of the tests are suppose to fail at this point.

As you accomplish tasks when writing your code, test it (test as you go and test often). It's time to write the PowerShell code for what I think will accomplish the task at hand:

 1function Get-NumberParity {
 2    [CmdletBinding()]
 3    param (
 4        [Parameter(Mandatory,
 5                   ValueFromPipeline)]
 6        [int[]]$Number
 7    )
 8
 9    PROCESS {
10        foreach ($n in $Number) {
11            switch ($n % 2) {
12                0 {[string]'Even'; break}
13                1 {[string]'Odd'; break}
14            }
15        }
16    }
17}

Now to test our newly created Get-NumberParity function by running Invoke-Pester:

pester11.jpg

Agh...one of the test is still failing! The problem with using the modulus operator is that negative odd numbers return -1 instead of 1.

pester12.jpg

By the way, we're lucky we decided to write tests for this because there's no telling how long this code might have been in production before this problem was detected.

We could add a line to our switch statement to account for a value of -1, but that wouldn't seem to be as clean as it could be and there has to be a better way.

We could divide the number by two and see if it is an integer (has a remainder or not):

 1function Get-NumberParity {
 2    [CmdletBinding()]
 3    param (
 4        [Parameter(Mandatory,
 5                   ValueFromPipeline)]
 6        [int[]]$Number
 7    )
 8
 9    PROCESS {
10        foreach ($n in $Number) {
11            switch ($n / 2) {
12                {$_ -is [int]} {[string]'Even'; break}
13                {$_ -isnot [int]} {[string]'Odd'; break}
14            }
15        }
16    }
17}

That code meets our specifications and passes our tests:

pester13.jpg

I guess I'm too much of a perfectionist though because there still seems like there should be a better way to accomplish this task and indeed there is, at least in my opinion. We'll use one of the bitwise operators:

 1function Get-NumberParity {
 2    [CmdletBinding()]
 3    param (
 4        [Parameter(Mandatory,
 5                   ValueFromPipeline)]
 6        [int[]]$Number
 7    )
 8
 9    PROCESS {
10        foreach ($n in $Number) {
11            switch ($n -band 1) {
12                0 {[string]'Odd'; break}
13                1 {[string]'Even'; break}
14            }
15        }
16    }
17}

This code also passes all of our tests with Pester:

pester13.jpg

An additional benefit is that you'll save the tests along with the PowerShell code and in the future when a new feature is needed, you can not only add the new feature using the same test-driven development approach, but you'll have the original tests to rerun to make sure you don't break anything in the process of adding new functionality.

The next time your boss shows up and wants to know what you're working on, tell him you're refactoring your PowerShell code.

µ