Write Dynamic Unit Tests for your PowerShell Code with Pester

I wrote a blog article on: PowerShell Script Module Design: Placing functions directly in the PSM1 file versus dot-sourcing separate PS1 files earlier this year and I've moved all of my PowerShell script modules to that design and while today's blog article isn't part of a series, that previous one is recommended reading so you're not lost when trying to understand what I'm attempting to accomplish.

Most unit tests that I've seen created with Pester for testing PowerShell code are very specific, the tests generally have hard coded values in them, and live in source control along with the code that they're designed to test. What if I wanted to create unit tests for each of my modules to make sure they meet a certain standard? What if I could write them generically so the same or similar test wasn't being written over and over again since creating redundant code sounds like a lot of technical debt and something I try to avoid when writing PowerShell code so why not apply the same principles when writing Pester tests?

As described in that previously referenced blog article, I've moved to separating all of my functions into separate PS1 files that are dot-sourced from a PSM1 script module file. All of the functions must be listed in the FunctionsToExport section of the module manifest file (or exported via Export-ModuleMember in the PSM1 file) otherwise module auto-loading won't work properly. In my module design scenario, the base file name of each PS1 file matches the corresponding function name so I simply need to create a Pester test to compare the exported functions to the list of those base file names. Sounds simple enough, right?

I started by writing a Pester test, then wrapping it inside a function along with adding parameterization so it could be called just like any other function, accept dynamic input, and provide the same type of output that would normally be seen with any Pester unit test. This is some of the bonus content that I showed last week at the end of one of my presentations at the PowerShell and DevOps Global Summit 2016.

 1#Requires -Version 3.0 -Modules Pester
 2function Test-MrFunctionsToExport {
 3
 4<#
 5.SYNOPSIS
 6    Tests that all functions in a module are being exported.
 7
 8.DESCRIPTION
 9    Test-MrFunctionsToExport is an advanced function that runs a Pester test against
10    one or more modules to validate that all functions are being properly exported.
11
12.PARAMETER ManifestPath
13    Path to the module manifest (PSD1) file for the modules(s) to test.
14
15.EXAMPLE
16    Test-MrFunctionsToExport -ManifestPath .\MyModuleManifest.psd1
17
18.EXAMPLE
19    Get-ChildItem -Path .\Modules -Include *.psd1 -Recurse | Test-MrFunctionsToExport
20
21.INPUTS
22    String
23
24.OUTPUTS
25    None
26
27.NOTES
28    Author:  Mike F Robbins
29    Website: http://mikefrobbins.com
30    Twitter: @mikefrobbins
31#>
32
33    [CmdletBinding()]
34    param (
35        [Parameter(ValueFromPipeline)]
36        [ValidateScript({
37            Test-ModuleManifest -Path $_
38        })]
39        [string[]]$ManifestPath
40    )
41
42    PROCESS {
43        foreach ($Manifest in $ManifestPath) {
44
45            $ModuleInfo = Import-Module -Name $Manifest -Force -PassThru
46
47            $PS1FileNames = Get-ChildItem -Path "$($ModuleInfo.ModuleBase)\*.ps1" -Exclude *tests.ps1, *profile.ps1 |
48                            Select-Object -ExpandProperty BaseName
49
50            $ExportedFunctions = Get-Command -Module $ModuleInfo.Name |
51                                 Select-Object -ExpandProperty Name
52
53            Describe "FunctionsToExport for PowerShell module '$($ModuleInfo.Name)'" {
54
55                It 'Exports one function in the module manifest per PS1 file' {
56                    $ModuleInfo.ExportedFunctions.Values.Name.Count |
57                    Should Be $PS1FileNames.Count
58                }
59
60                It 'Exports functions with names that match the PS1 file base names' {
61                    Compare-Object -ReferenceObject $ModuleInfo.ExportedFunctions.Values.Name -DifferenceObject $PS1FileNames |
62                    Should BeNullOrEmpty
63                }
64
65                It 'Only exports functions listed in the module manifest' {
66                    $ExportedFunctions.Count |
67                    Should Be $ModuleInfo.ExportedFunctions.Values.Name.Count
68                }
69
70                It 'Contains the same function names as base file names' {
71                    Compare-Object -ReferenceObject $PS1FileNames -DifferenceObject $ExportedFunctions |
72                    Should BeNullOrEmpty
73                }
74
75            }
76
77        }
78
79    }
80
81}

I'll run this test on my MrToolkit module that's part of my PowerShell repository on GitHub. The funny thing is this particular function is part of that module so in a way, it's testing itself:

1Test-MrFunctionsToExport -ManifestPath .\GitHub\PowerShell\MrToolkit\MrToolkit.psd1

dyn-pester1a.jpg

Based on the previous results, the first test tells me that 21 functions should have been exported but only 20 were so the test failed. Based on the failure of the second and fourth test and success of the third one, it's easy to determine the problem is that more functions exist in the module directory than are specified in the manifest.

After updating the FunctionsToExport section in the module manifest file, the test completes successfully:

1Test-MrFunctionsToExport -ManifestPath .\GitHub\PowerShell\MrToolkit\MrToolkit.psd1

dyn-pester2a.jpg

The Test-MrFunctionsToExport function can also be used to test multiple modules at the same time as in this scenario where I'll test all of the modules in my GitHub repositories where the repository starts with the letter A. Notice that the Test-MrFunctionsToExport function accepts pipeline input:

1Get-ChildItem -Path U:\GitHub\a* -Include *.psd1 -Recurse | Test-MrFunctionsToExport

dyn-pester3a.jpg

Separating functions into PS1 files and dot-sourcing them from a PSM1 file definitely adds more complexity than placing them directly inside a module's PSM1 file, but in my opinion the benefits outweigh the disadvantages.

I think of Pester tests as not only a form of beginning with the end in mind, but being intentional about determining if the desired end result is going to be produced by whatever tool I'm building with PowerShell.

µ