PowerShell How-To
Designing Testable PowerShell Code
Keeping in mind that your code will eventually need to pass the Pester test will help when finalizing your scripts.
- By Adam Bertram
- 05/04/2017
One thing's for certain; the code you write today in PowerShell is probably not "test ready." Why do I say that? The reason is that most of us write PowerShell code just to get things done. PowerShell has traditionally been about "scripting" and not actual "software development." We write scripts to do ad-hoc things to make our lives easier and move on. But, with Microsoft getting fully behind PowerShell and even forcing some products to be managed with it, people are investing more in the language and depending on their scripts and modules to maintain mission-critical processes. Once a piece of code begins to ensure production systems are available, the time comes to write tests.
The de-facto standard for writing tests for PowerShell is Pester. If you haven't already got up to snuff on Pester I highly recommend checking out The Pester Book. It goes into all the details you'd need to know about this great testing product.
Let's say you know how to use Pester, begin to write tests for your existing code but soon get discouraged. You'll usually find out that the way in which your code is written doesn't behave nicely with Pester. Rather than attempting to write tests for existing code, it's always a good idea to take a step back and decide if your code could have been written better in the first place.
First of all, there are lots of ways code can be considered nearly untestable with Pester while other methods may just prove to be unwieldy.
Use Functions
To perform typical unit-testing tactics, Pester needs to be able to "control" code. To write good tests, you must be able to selectively pick out which pieces of code to test and when. The tool to do this with Pester is mocking. Mocking allows you to "overwrite" existing functions and replace them with user-defined code. This means if you've got code that looks like this to find computer accounts in Active Directory, it's going to have to be refactored.
$Searcher = New-Object DirectoryServices.DirectorySearcher
$Searcher.Filter = '(&(objectCategory=person)(anr=gusev))'
$Searcher.SearchRoot = 'LDAP://OU=Laptops,OU=Computers,DC=consoso,DC=com'
$Searcher.FindAll()
The reason the code above would need to be changed is that it's not leveraging any commands. It's creating an object and then using the object's properties and methods. Pester does not understand this, and thus this piece of code would be impossible to write useful tests for. Instead, we'd change that to using a cmdlet like the Get-AdUser command.
Get-AdComputer -Identity 'gusev'
Not only is the second method much cleaner and intuitive, but it's also very "Pesterable"!
Keep Functions Small
It's always a good practice to keep functions small. Functions should only do one little "thing, " and when building large scripts or modules, those functions will then call each other to perform some amount of work. However, if you haven't created any tests for your code before you might have gotten lazy and, if you did create functions, those functions might be gigantic. I've seen some functions that are hundreds of lines long! That's way too long. It's a good practice to keep them as small as absolutely possible. You'll find that when beginning to create tests for these large functions, you'll end up getting frustrated at how complicating this "testing thing" is. When, in reality, your code wasn't written right in the first place!
For example, take a fictional function that looks like this that contains hardly any other function references at all. The scripter just dumped a bunch of code into this function to do everything at once.
function Invoke-UserProvisioning {
param(
$FirstName,
$LastName
)
## Create the AD account
## Create the Exchange mailbox
## Create the home folder
## Create the DHCP reservation
## ...and on and on....
}
This function above is going to be terrible to write tests for. It's going to have a lot of mocking involved and accounting for so many different variables. Instead, why not solve this problem by creating separate functions for this task and then calling them together in a script?
function New-AcmeADUser {
param()
}
function New-AcmeExchangeMailbox {
param()
}
function New-AcmeHomeFolder {
param()
}
function New-AcmeDhcpReservation {
param()
}
Granted, this may not be able to be split up into this many functions, but you should see where I'm going here. Break out your functions, so they are only performing one, succinct task. Don't try to boil the ocean with a single function. The code will be unmaintainable in the long run, and your tests will be not only difficult to build but also difficult to maintain.
About the Author
Adam Bertram is a 20-year veteran of IT. He's an automation engineer, blogger, consultant, freelance writer, Pluralsight course author and content marketing advisor to multiple technology companies. Adam also founded the popular TechSnips e-learning platform. He mainly focuses on DevOps, system management and automation technologies, as well as various cloud platforms mostly in the Microsoft space. He is a Microsoft Cloud and Datacenter Management MVP who absorbs knowledge from the IT field and explains it in an easy-to-understand fashion. Catch up on Adam's articles at adamtheautomator.com, connect on LinkedIn or follow him on Twitter at @adbertram or the TechSnips Twitter account @techsnips_io.