When a Hashtable
is used as input for Should
, Pester outputs only the typename instead of the content:
Describe 'test' {
It 'test case' {
$ht = @{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
}
Output:
Expected $null or empty, but got @(System.Collections.Hashtable).
Expected output like:
Expected $null or empty, but got @{ foo = 21; bar = 42 }.
Looking at Pester source, the test input is formatted by private function Format-Nicely
, which just casts to String
if the value type is Hashtable
. This boils down to calling Hashtable::ToString()
, which just outputs the typename.
As a workaround I'm currently deriving a class from Hashtable
that overrides the ToString
method. Before passing the input to Should
, I cast it to this custom class. This makes Pester call my overridden ToString
method when formatting the test result.
BeforeAll {
class MyHashTable : Hashtable {
MyHashTable( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
}
Describe 'test' {
It 'test case' {
$ht = @{ foo = 21; bar = 42 }
[MyHashTable] $ht | Should -BeNullOrEmpty
}
}
Now Pester outputs the Hashtable
content in JSON format, which is good enough for me.
Is there a more elegant way to customize Pester output of Hashtable
, which doesn't require me to change the code of each test case?
A cleaner (albeit more lengthy) way than my previous answer is to write a wrapper function for Should
.
Such a wrapper can be generated using System.Management.Automation.ProxyCommand
, but it requires a little bit of stitchwork to generate it in a way that it works with the dynamicparam
block of Should
. For details see this answer.
The wrappers process
block is modified to cast the current pipeline object to a custom Hashtable
-derived class that overrides the .ToString()
method, before passing it to the process
block of the original Should
cmdlet.
class MyJsonHashTable : Hashtable {
MyJsonHashTable ( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
Function MyShould {
[CmdletBinding()]
param(
[Parameter(Position=0, ValueFromPipeline=$true, ValueFromRemainingArguments=$true)]
[System.Object]
${ActualValue}
)
dynamicparam {
try {
$targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function, $PSBoundParameters)
$dynamicParams = @($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
if ($dynamicParams.Length -gt 0)
{
$paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
foreach ($param in $dynamicParams)
{
$param = $param.Value
if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
{
$dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
$paramDictionary.Add($param.Name, $dynParam)
}
}
return $paramDictionary
}
} catch {
throw
}
}
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process {
try {
# In case input object is a Hashtable, cast it to our derived class to customize Pester output.
$item = switch( $_ ) {
{ $_ -is [Hashtable] } { [MyJsonHashTable] $_ }
default { $_ }
}
$steppablePipeline.Process( $item )
} catch {
throw
}
}
end {
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
To override Pesters Should
by the wrapper, define a global alias like this:
Set-Alias Should MyShould -Force -Scope Global
And to restore the original Should
:
Remove-Alias MyShould -Scope Global
Notes:
GetCommand()
from Should
to Pester\Should
to avoid recursion due to the alias. Not sure if this is actually necessary though.