问题
Problem
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 }.
Cause
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.
Workaround
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.
Question
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?
回答1:
Somewhat of a hack, override Pester's private Format-Nicely
cmdlet by defining a global alias of the same name.
BeforeAll {
InModuleScope Pester {
# HACK: make private Pester cmdlet available for our custom override
Export-ModuleMember Format-Nicely
}
function global:Format-NicelyCustom( $Value, [switch]$Pretty ) {
if( $Value -is [Hashtable] ) {
return $Value | ConvertTo-Json
}
# Call original cmdlet of Pester
Pester\Format-Nicely $Value -Pretty:$Pretty
}
# Overrides Pesters Format-Nicely as global aliases have precedence over functions
New-Alias -Name 'Format-Nicely' -Value 'Format-NicelyCustom' -Scope Global
}
This enables us to write test cases as usual:
Describe 'test' {
It 'logs hashtable content' {
$ht = @{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
It 'logs other types regularly' {
$true | Should -Be $false
}
}
Log of 1st test case:
Expected $null or empty, but got @({ "foo": 21, "bar": 42 }).
Log of 2nd test case:
Expected $false, but got $true.
回答2:
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:
- I have also changed the argument of
GetCommand()
fromShould
toPester\Should
to avoid recursion due to the alias. Not sure if this is actually necessary though. - A recent version of Pester is required. Failed with Pester 5.0.4 but tested successfully with Pester 5.1.1.
来源:https://stackoverflow.com/questions/65602360/show-content-of-hashtable-when-pester-test-case-fails