Use Pester to author your PowerShell scripts

Pester is a great module for testing your PowerShell scripts and modules. It has great mocking support, a test drive for setting up isolated files and supports a lot of assertions. Since the importance of PowerShell growths, testing your scripts is a must.

This is part 2 of the series “Develop next level PowerShell with Visual Studio and Pester”. It assumes that you have a Visual Studio solution with a basic Pester Test in GitHub.

Post Content
Part 1: Develop next level PowerShell with Visual Studio and Pester In this post I focus on creating the project in Visual Studio and interacting with a source control system like git.
Part 2: Use Pester to author your PowerShell scripts using TDD/BDD This post focuses on writing PowerShell scripts or modules using Pester as a TDD/BDD framework.
Part 3: Run your Pester tests in a VSTS build In this post I show you how you can run you tests in a continuous integration build and display the build status with a badge in your repository.

 

Pester basics

If you create a new test in Visual Studio, it looks like this:

Describe "PowerShellTest1" {
    Context "Exists" {
        It "Runs" {
        }
    }
}

As you can see you have three levels:

Describe: This is the name of the test displayed in test explorer. It is not ONE test in the test results. It’s more a container scope for many tests that executes as a unit.

Context: Like “Describe” the Context is a container for tests. You can mock functions or have test files in the scope of a context.

It: “It” is the actual test. You add an assertion here to test the state of your system. “It” is also a scope for mocking like context.

You can use these elements to structure your tests in a BDD style after the GivenWhenThen pattern.

$project = (Split-Path -Parent $MyInvocation.MyCommand.Path).Replace(".Tests", "")
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".tests.", ".")
. "$project\$sut"

Describe "AnswerToUltimateQuestion" {
    Context "Given an enormous supercomputer named Deep Thought, when we ask the ultimate question about life, the universe, and everything" {
        It "should return 42" {
			       AnswerToUltimateQuestion | Should Be 42
		     }
    }
}

At the top of the file you have to include your sut (system or script under test). Depending of your project structure this may vary. We already did this in Part 1 – so I will not cover this here.

In the “IT” we have to add an assertion. We do this by piping a result to the “Should” function. The should function can have a lot of different arguments. The most common is “Be” which compares the result against a fix value. Other possibilities are “BeGraterThen”, “BeLessThen”, “BeNullOrEmpty”, “ByOfType”, “Contain”, “ContainExacly”, “Match”, “MatchExactly” and “Throw”. If you use “Throw” you have to pipe a ScriptBlock – you do this by wrapping your function in brackets.

Mocking with Pester

Let’s assume we have a basic function that takes a path as the parameter and returns the file length.

function FunctionWithParameter {

    [CmdletBinding()]
    [OutputType([int])]
    Param (
		[Parameter(Mandatory=$true, Position=0)]
		[string]$Path
	)

	if (Test-Path $Path) {
		return (Get-ChildItem $Path).Length
	}else{
		return 0
	}
}

The function internally calls the functions Test-Path and Get-ChldItem. We can easily mock ANY function by just calling the “Mock” function. The Mock function takes the name of the function as the first parameter and the replacement as the second: Mock Test-Path { $false }

This will cause all call to Test-Path in the current context to return false. If you want to verify, that a mock was called you have to add the switch –Verifiable to the mock. You can then use Assert-MockCalled to verify that the mock was called (exactly or minimum).

Context "When the file does not exist" {

	Mock Test-Path { $false } -Verifiable

	$actual = FunctionWithParameter -Path "does not exist"

	It "should return zeo" {

		$actual | Should Be 0
	}

	It "should call Test-Path once" {
			
		Assert-MockCalled Test-Path -Times 1 -Exactly
	}
}

 

If you only want to mock a function, if it is called with a special set of parameters, you can use –ParameterFilter. The ParameterFilter takes a script block with an expression that uses a variable for each parameter and returns true or false. For example, “-ParameterFilter { $Path -eq “file does exist” }” compares the parameter “Path” with the value “file does exist”. You can also use ParameterFilter with the Assert-MockCalled function instead of the Mock function.

If you have to mock functions that return complex types, it’s a big advantage, that PowerShell is not type save. You normally can just return a Hashtable with the necesarry properties. So instead of mocking a complex FileInfo object, we can just mock Get-ChildItem with: Mock Get-ChildItem { @{ Length = 42 } } and return an object that has the property “Length” with a fix value of 42.

Context "When the file does exist" {

	Mock Test-Path { $true } -Verifiable -ParameterFilter { $Path -eq "file does exist" }
	Mock Get-ChildItem { @{ Length = 42 } } -Verifiable

	$actual = FunctionWithParameter -Path "file does exist"

	It "should return the file length" {

		$actual | Should Be 42
	}

	It "should call Test-Path once" {
			
		Assert-MockCalled Test-Path -Times 1 -Exactly
	}
}

 

Working with Files

Working with files and directories is a very common practice in PowerShell and to mock all the functions around a file would be painful. Pester therefore has an isolated test drive where you can set up files and directories to use in your tests.

Let’s assume a function that returns a special value from a xml file.

function FunctionWithFileOperation {

    [CmdletBinding()]
    [OutputType([int])]
    Param (

		[Parameter(Mandatory=$true, Position=0)]
		[ValidateNotNullOrEmpty()]
		[ValidateScript({ Test-Path $Path })]
		[string]$Path
	)

	if (-not (Test-Path $Path)) { throw "The config file does not exist." }

	[xml]$xml = Get-Content $Path

	$id = $xml.App.Id

	if (-not $id) { throw "The config file is invalid." }

	return [int]$id
}

 

Of course you could mock all the function like Test-Path and Get-Content individually. But you can use the Setup function to create a file with a specific content. The file gets created in the $TestDrive – a special folder inside your temp folder. So all your tests are sure to work in an isolated drive with new folders and files.

Context "Given the file exists, when the config is valid" {
		
	Setup -File config.xml -Content "<App Id='42' />"

	It "retuns the id of the app" {

		FunctionWithFileOperation -Path $TestDrive\config.xml | Should Be 42
	}
}

Running your tests and debugging your scripts

You can run your test from within test explorer – but that does not yet support debugging your scripts and setting breakpoints. But what you can do is to execute your test script directly by hitting F5. You can add breakpoints and use Locals, Watches, Breakpoints and many more windows to help you debugging your script.

image

image

I think this should be enough to get you started with writing scripts with pester. In Part 3 I will show you how to enable continuous integration for your GitHub repository and to run your pester tests in a VSTS (formally VSO) build and to add a batch to your GitHub repo that indicates the last build status.

4 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s