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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | #Requires -Version 3.0 -Modules Pester function Test-MrFunctionsToExport { <# .SYNOPSIS Tests that all functions in a module are being exported. .DESCRIPTION Test-MrFunctionsToExport is an advanced function that runs a Pester test against one or more modules to validate that all functions are being properly exported. .PARAMETER ManifestPath Path to the module manifest (PSD1) file for the modules(s) to test. .EXAMPLE Test-MrFunctionsToExport -ManifestPath .\MyModuleManifest.psd1 .EXAMPLE Get-ChildItem -Path .\Modules -Include *.psd1 -Recurse | Test-MrFunctionsToExport .INPUTS String .OUTPUTS None .NOTES Author: Mike F Robbins Website: http://mikefrobbins.com Twitter: @mikefrobbins #> [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [ValidateScript({ Test-ModuleManifest -Path $_ })] [string[]]$ManifestPath ) PROCESS { foreach ($Manifest in $ManifestPath) { $ModuleInfo = Import-Module -Name $Manifest -Force -PassThru $PS1FileNames = Get-ChildItem -Path "$($ModuleInfo.ModuleBase)\*.ps1" -Exclude *tests.ps1, *profile.ps1 | Select-Object -ExpandProperty BaseName $ExportedFunctions = Get-Command -Module $ModuleInfo.Name | Select-Object -ExpandProperty Name Describe "FunctionsToExport for PowerShell module '$($ModuleInfo.Name)'" { It 'Exports one function in the module manifest per PS1 file' { $ModuleInfo.ExportedFunctions.Values.Name.Count | Should Be $PS1FileNames.Count } It 'Exports functions with names that match the PS1 file base names' { Compare-Object -ReferenceObject $ModuleInfo.ExportedFunctions.Values.Name -DifferenceObject $PS1FileNames | Should BeNullOrEmpty } It 'Only exports functions listed in the module manifest' { $ExportedFunctions.Count | Should Be $ModuleInfo.ExportedFunctions.Values.Name.Count } It 'Contains the same function names as base file names' { Compare-Object -ReferenceObject $PS1FileNames -DifferenceObject $ExportedFunctions | Should BeNullOrEmpty } } } } } |
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:
1 | Test-MrFunctionsToExport -ManifestPath .\GitHub\PowerShell\MrToolkit\MrToolkit.psd1 |
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:
1 | Test-MrFunctionsToExport -ManifestPath .\GitHub\PowerShell\MrToolkit\MrToolkit.psd1 |
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:
1 | Get-ChildItem -Path U:\GitHub\a* -Include *.psd1 -Recurse | Test-MrFunctionsToExport |
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.
µ
Like what you’ve done here. This can probably be take a little further. What you are after is like a almost editor/compile time checking for common issues like ReSharper in c# and linting tools in JavaScript. So adding your test cases to the likes of a PowerShell linting tool such as PSScriptAnalyzer would be great. This tool currently analyses all psm1 and ps1 files, be great to scope it to functions and module manifests as well.