From Learn PowerShell Scripting in a Month of Lunches by Don Jones and Jeffery Hicks

As we move into a DevOps-y world, one of the things you’ll need to start thinking about is how you’ll test your scripts. Here’s the deal: nobody likes a broken script in production. And although you might run a few tests on your script, you – or someone else – might also modify your script at some point, necessitating a re-test. Or, you might find some odd condition under which your script fails – well, you certainly don’t want to forget to test that condition again in the future, do you? In this article, we’ll talk about automated unit testing for PowerShell scripts.


Save 37% on Learn PowerShell Scripting in a Month of Lunches. Just enter code fccjones into the discount code box at checkout at manning.com.


The vision

Here’s where we want to get you to:

  1. You write up some code, or modify some old code.
  2. You check your code into a source control repository.
  3. The repository triggers a continuous integration pipeline. Usually incorporating third-party tools (TeamCity is the one used on PowerShell.org’s free build service, for example), the pipeline builds out a virtual machine to test your script. The pipeline copies your script into the virtual machine, and runs several automated tests. If the tests fail, you get an email telling you what happened.
  4. If the tests pass, your code is deployed to a deployment repository (maybe PowerShellGallery.com, or maybe a private repo), making it available for production.

Step number three is what we call The Miracle, as in, “you check in your code, The Miracle occurs, and your code is deployed.” Step three is entirely automated – and every tool you need to make step three happen exists today. But the bit you need to contribute to The Miracle is a way of automatically testing your code. That way, any time you revise your code, The Miracle can quickly re-test it, make sure it’s working, and deploy it – or bounce it back to you for fixes.

 

The problems with manual testing

We’re sure that you’ve manually tested your scripts before – possibly even as you wrote scripts for this book. And this is fine – you should definitely test your code as you go. But there are some real problems with manual testing:

  • You’re lazy, as are we. You’re not going to run every possible test every time through. And it’ll always be the one test you didn’t run that would have caught the error in your code.
  • It’s time-consuming. Even if you’re not flat-out lazy, manual testing takes time and effort that could be better spent elsewhere.
  • It doesn’t tend to learn. It’s not like you have a huge list of tests which you know you need to run through; you’re probably doing what we do and thinking, “well, I’ll run it with parameters one time, pipe some stuff to it another time, and this is probably good.” If you fix a problem, you might test that specific problem right then, but you might or might not re-test that specific problem in the future.
  • It’s manual. You can’t achieve The Miracle with manual testing. Remember, PowerShell is all about automation – why should testing be excluded from that?

 

The benefits of automated testing

Automated testing, on the other hand, rocks. Mainly because it’s automatic. Also, because it learns – if you run across some weird condition that broke your code one time, you add a test for that condition to your test script, and then you’ll never “forget” to test that weird condition again. Automated tests, therefore, serve as a kind of documented institutional memory. Even if someone else modifies your script, and they didn’t know about that weird condition, the automated test will still have their back, and make sure the weird condition gets tested.

Automated testing can even move you to a world of Test-Driven Development (TDD). Let’s say you decide to add a new feature to a command. Rather than breaking out the command’s script and starting to modify it, you’d first write a few tests to test the proposed new feature. Those tests would describe how you want the new feature to work, and they serve as a kind of functional specification. Initially, the tests will fail, because you haven’t coded up the new feature yet. But then you start coding the new feature, and you keep coding until all the tests pass. If you did a good job on the tests themselves, then you’ll know your feature is working correctly.

 

Introducing Pester

Pester (“PowerShell Tester,” sort of) is an open-source project which is bundled with Windows 10 and later (although newer versions can be found in the PowerShell Gallery). It’s an automated unit-testing framework for PowerShell. You write your tests “in” Pester and Pester runs your tests for you. Pester’s basic documentation is in the wiki of its GitHub repository, at https://github.com/pester/Pester/wiki.

BUT WAIT, THERE’S MORE This article is going to be the barest introduction to Pester, with an intent of whetting your appetite. You need to go read the docs to discover all of the other cool things Pester can do which we won’t mention here.

As an interesting side note, Microsoft uses Pester to automate the testing of their own PowerShell resources. You’ll find all kinds of Pester tests included in the various open-source PowerShell-based components that the PowerShell team wrote. These tests number in the thousands! That, if nothing else, should tell you how important and well-regarded Pester is to the PowerShell community.

 

Coding to be tested

If you want to have a successful relationship with Pester, you must start writing commands and scripts that lend themselves to testing. Follow all of the advice that we’ve provided in this article. Specifically, focus on making self-contained, single-task tools. Tools that do eight different things are going to be hard to test, because you’re going to need to test every one of those eight things in all their possible combinations and permutations. A tool that does one thing, on the other hand, is a lot easier to write tests for.

You also need to recognize that Pester is a PowerShell testing framework – not COM, not .NET, not SQL Server, not anything else. It works best when it only needs to deal with PowerShell commands. If you’re following our advice then you’re writing PowerShell commands to “wrap” any non-PowerShell code you may need to use, meaning at the end of the day you’re only dealing with PowerShell commands. In that scenario, you and Pester will get along fine.

 

What do you test?

Because this is intended to be a bare-bones introduction to Pester, we’re going to fudge a few terms that the automated testing industry takes seriously. Specifically, we’re going to use the terms unit testing and integration testing to lay out a couple of scenarios, to help you understand what to write tests for.

Integration tests

An integration test tests the end-state of your command. If you write a command to create a SQL Server database, an integration test runs the command, and then checks to see if the database exists. An integration test tests the final impact of your code on the world at large. An integration test treats your code as a kind of “black box,” meaning it doesn’t necessarily know what’s happening inside your code. It doesn’t test to see if you instantiated the right .NET classes to connect to SQL Server, and it doesn’t test to see if the username and password you provided work. It only checks the end result.

Integration tests are a good thing. But they’re not the only thing.

Unit tests

Unit tests are more granular, and they’re a bit trickier to imagine. They’re unconcerned with whether your code accomplishes anything – they only want to make sure that your code runs. For example, you might have a command that can change a service’s startup mode and logon password, or it can do one of those things, depending on which parameters are provided to it. A unit test would run it in all three ways, and ensure that all of the internal logical decisions and code paths run correctly. Whether any particular service is changed isn’t the concern of the unit test.

Often times, you’ll write unit tests and integration tests. There might be times when you only write unit tests, because you’re only concerned that your code follows the correct paths and logical decisions, and perhaps because doing something – which is what an integration test requires – would damage or negatively impact your environment. This can be a hard concept for folks to grasp. For example, if you write a command that reboots a computer, how could you not check to see if the computer rebooted? Well, it depends. If you were calling a command like Restart-Computer, then you wouldn’t need to test that – you’d want to test your code that led up to Restart-Computer being called. Which brings us to our next point.

Don’t test what isn’t yours

Particularly with unit tests, your goal is to test your code. The Restart-Computer command isn’t your code. It’s Microsoft’s code. If Microsoft’s code is broken, it isn’t your problem. Your unit test is there to make sure the code you can control is working correctly. In fact, let’s take that exact scenario and turn it into a Pester example.

 

Writing a basic Pester test

Let’s start with the command shown in Listing 1. It’s deliberately simplistic, allowing us to focus on the unit testing aspect of this.

Listing 1 Starting with a command that we want to test

 
 function Set-ComputerState {
     [CmdletBinding()]
     Param(
         [Parameter(Mandatory=$True,
                    ValueFromPipeline=$True,
                    ValueFromPipelineByPropertyName=$True)]
         [string[]]$ComputerName,
  
         [Parameter(Mandatory=$True)]
         [ValidateSet('Restart','Shutdown')]
         [string]$Action,
  
         [switch]$Force
     )
     BEGIN {}
     PROCESS {
  
         ForEach ($comp in $ComputerName) {
  
             $params = @{'Computername' = $comp}
  
             # force?
             if ($force) {
                 $params.Add('Force',$true)
             }
  
             # which action?
             If ($Action -eq 'Restart') {
                 Write-Verbose "Restarting $comp (Force: $force)"
                 Restart-Computer @params
             } else {
                 Write-Verbose "Stopping $comp (Force: $force)"
                 Stop-Computer @params
             }
         }
  
     } #PROCESS
     END {}
 }
 

READ IT NOW Take some time to read through this command, and develop an expectation for what it does and how it works. You might think of other, and even better ways, to accomplish that task. We’ve gone this route to help create a good illustration of Pester testing.

When it comes to unit testing, we know right away two things that we won’t be testing: whether Restart-Computer or Stop-Computer work. “But wait!” you might cry. “Those are the only two things that do anything!” Correct – and if we were writing an integration test, that would matter. Unit tests don’t care about the end result; they care about whether our code runs correctly. Because those two commands aren’t our code, we’re not going to unit test them.

Inside or outside?

Again, both kinds of test are important – but for now, we’re focused on unit tests.

Creating a fixture

We’re going to start by loading the Pester module and asking it to create a new test fixture for us.

 
 PS C:> Import-Module Pester
 PS C:> Mkdir example
 PS C:> New-Fixture –Path example –Name Set-ComputerState
 

NOTE This assumes Pester is available on your system, and on Windows 10 or later it will be, by default. If you can’t load the module, you need to install it first, by running Install-Module Pester.

This new fixture is a couple of blank files, one for our code (Set-ComputerState.ps1) and one for our tests (Set-ComputerState.Tests.ps1). We’re going to open both in VS Code. We’ll paste our function into Set-ComputerState.ps1 as a starting point, replacing the empty Set-ComputerState function which is already there.

TRY IT NOW Please follow along and get your own fixture set up and paste Listing 1 into the code script.

The test script – which you should create on your own should look like this:

 
 $here = Split-Path -Parent $MyInvocation.MyCommand.Path
 $sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace
 [CA]'\.Tests\.', '.'
 . "$here\$sut"
  
 Describe "Set-ComputerState" {
     It "does something useful" {
         $true | Should Be $false
     }
 }
  
 

Aside from the commands at the top, which “link” this to the code script, we’ve got two sections:

  • The Describe block is designed to contain a set of tests. These all execute within the same scope as each other. Scoping in Pester is both complex and powerful, and as you get into more complex tests you’ll often define multiple Describe blocks. For now, we’ll stick with this one.
  • The It block represents a single test, which our code either passes or fails. A Describe block often contains many It blocks, with each It testing some specific, discrete condition.

Writing the first test

We’re going to modify the provided It block to test something.

 
 Describe "Set-ComputerState" {
     It "accepts one computer name" {
         Set-ComputerState –computername SERVER1 –Action restart |
         Should Be $true
     }
 }
  
 

This is the basic model for an It block: you run something, and then you tell Pester what the result should have been. What we’ve written here won’t work, though, because our Set-ComputerState function never outputs anything to the pipeline. Therefore, it isn’t piping anything to Should, and Should definitely won’t be looking at a $true value as we’ve implied. This brings us to a heck of a problem – for a function that doesn’t produce any output, and where we’re not attempting to test if it does anything, how the heck, exactly, do we test the dang thing?

Our dilemma, stated more specifically, is that we need to see how many times Restart-Computer is called, without calling Restart-Computer. Tricky. And the answer to that trick is a key element of Pester: the Mock.

Creating a mock

Many times, in testing, you’ll want to have some command seem to run, but not run. For example, you might need to have Import-CSV import a specific CSV file, but you don’t want to create the file. Or, in our case, you want Restart-Computer to seem to run, allowing us to figure out if our code tried to run it, but we by no means want to restart a computer. This is where Pester’s mocking comes into play. They create a “fake” replacement for an existing command, and that fake can do whatever you like.

 
 Describe "Set-ComputerState" {
  
     Mock Restart-Computer { return 1 }
     Mock Stop-Computer { return 1 }
  
     It "accepts one computer name" {
         Set-ComputerState -computername SERVER1 -Action Restart |
         Should Be 1
     }
 }
  
 

Our “fake” version of Restart-Computer outputs “1”. It won’t restart any computers – it’ll output “1”. If it’s called one time, the result of Set-ComputerState should be 1. We’ve told Pester as much with our It block. Let’s try running this simple test to see if it works. From our example folder, which contains our tests script, we need to run Invoke-Pester:

 
 Describing Set-ComputerState
  [+] accepts one computer name 678ms
 Tests completed in 678ms
 Passed: 1 Failed: 0 Skipped: 0 Pending: 0 Inconclusive: 0
 

TRY IT NOW The results are better in full color; see if you can get similar output by copying what we’ve done this far.

The [+] tells us that our single test passed.

We’re going to stop here. Hopefully, you’ve seen enough to pique your interest in how Pester can help you with automating your PowerShell script testing.


If you want to learn more about the book, check it out on here and see this Slide deck.