Yesterday I shared some PowerShell code I wrote to discover tags in a Pester test. It works nicely and I have no reason to complain. But as usual, there is never simply one way to do something in PowerShell. I got a suggestion from @FrodeFlaten on Twitter on an approach using the new configuration object in Pester 5.2. I'll readily admit that I am still getting up to speed on the latest version of Pester. That's one of my goals for this year, so this was a great chance to learn something new.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
PesterConfiguration
In previous versions of Pester, you could control Invoke-Pester through a set of parameters. Now we can use a configuration object that contains all the test settings. You can create a default object with New-PesterConfiguration.
The suggestion was to configure Pester to skip running all tests and pass the test object to the pipeline. The configuration object has a set of nested properties. Here's Run.
I can now modify the configuration object.
$config.Run.SkipRun = $true
$config.run.path = 'C:\scripts\PSFunctionTools\tests\psfunctiontools.tests.ps1'
$config.run.PassThru = $True
$config.Output.Verbosity = "none"
The properties should be self-explanatory. Since I'm not really running the test, I don't need to see any test results. I'll run Invoke-Pester using the configuration object.
$r = Invoke-Pester -Configuration $config -WarningAction SilentlyContinue
I'm using the warning action to suppress any deprecation messages. Not really necessary, but it makes for a cleaner experience.
The Pester Test Object
Because I used a Passthru configuration, I get a result like this:
I can get tags from all individual tests easily enough.
$($r.tests.tag).Where({$_}).foreach({$_.toLower()}) | Select-Object -unique
I'm doing some filtering and processing to get a unique list of tags.
But containers like Describe and Context can also have tags.
What you see are all Describe blocks and their tags. But that first Describe block has a nested Context blog which also has a tag. I need to recurse through containers to identify their tags.
function _recurseBlock {
Param([object]$block)
$block.tag
if ($block.blocks) {
foreach ($item in $block.blocks) {
_recurseBlock $item
}
}
}
foreach ($block in $r.containers.blocks) {
_recurseBlock $block
}
The nested Contex block has a tag of "structure". Of course, I can process this list to get unique items. Combine these results with the test tags and I should have everything I need.
A Revised PowerShell Function
With all of this in mind, I wrote a revised version of my function.
#requires -version 7.1
#requires -module @{ModuleName = 'Pester';ModuleVersion='5.2'}
Function Get-PesterTag {
#thanks to https://twitter.com/FrodeFlaten for the suggestion
[cmdletbinding()]
[OutputType("pesterTag")]
Param(
[Parameter(
Position = 0,
Mandatory,
HelpMessage = "Specify a Pester test file",
ValueFromPipeline
)]
[ValidateScript({
#validate file exits
if (Test-Path $_) {
#now test for extension
if ($_ -match "\.ps1$") {
$True
}
else {
Throw "The filename must end in '.ps1'."
}
}
else {
Throw "Cannot find file $_."
}
})]
[string]$Path
)
Begin {
Write-Verbose "[$((Get-Date).TimeofDay) BEGIN ] Starting $($myinvocation.mycommand)"
$config = New-PesterConfiguration
$config.Run.SkipRun = $true
$config.run.PassThru = $True
$config.Output.Verbosity = "none"
#a private helper function to recurse through Describe and Context test blocks
function _recurseBlock {
Param([object]$block)
$block.tag
if ($block.blocks) {
foreach ($item in $block.blocks) {
_recurseBlock $item
}
}
}
#Initiate a list to hold tags
$list = [system.collections.generic.list[string]]::new()
} #begin
Process {
$Path = Convert-Path $path
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Getting tags from $Path"
$config.run.path = $path
Write-Verbose "[$((Get-Date).TimeofDay) PROCESS] Invoking Pester"
$r = Invoke-Pester -Configuration $config -WarningAction SilentlyContinue
#get Test tags and add unique items to the list
$($r.tests.tag).Where({ $_ }).foreach({
$t = $_.toLower();
if (-Not ($list.Contains($t))) {
$list.add($t)
} })
#get block tags
foreach ($block in $r.containers.blocks) {
_recurseBlock $block | ForEach-Object {
$t = $_.toLower()
#add item to the list if not already there
if (-Not ($list.Contains($t))) {
$list.add($t)
}
} #foreach tag
} #foreach block
if ($list.count -gt 0) {
[pscustomobject]@{
PSTypename = "pesterTag"
Path = $Path
Tags = $List | Sort-Object
}
}
else {
Write-Warning "No tags found in $Path"
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeofDay) END ] Ending $($myinvocation.mycommand)"
} #end
} #close Get-PesterTag
The function still writes an object to the pipeline but it uses a different approach to get the same results. However, there are tradeoffs.
My first function using regular expressions runs very fast. It processed this test file in 0.54ms. This version using the Pester configuration object takes 0.9ms. Frankly, I'm not going to quibble over this minute difference. In fact, this new version is going to be more accurate as it is processing the actual test file. My version using the PowerShell AST might not differentiate between live code and something that might have been commented out (I'd have to test that to be sure) so I'll most likely stick with this new version.
In fact, now that I know more about the Pester configuration object a number of ideas are sparking. I'm confident I'll be back.
Oh, before I forget. This new function only works in PowerShell 7. At least as written. I think Microsoft made some changes under the hood on how it unrolls collections of objects which is how I can easily get the tag property. If you are stuck with Windows PowerShell, you would need to use the AST version. Or feel free to modify this code to work in Windows PowerShell. A big goal for me this year is to leave Windows PowerShell behind. I'd rather spend my time learning new things instead of struggling with backward compatibility. I hope you can move forward with me.
1 thought on “Discovering Pester Tags Revisited”
Comments are closed.