Show content of hashtable when Pester test case fails

让人想犯罪 __ 提交于 2021-02-05 07:55:23

问题


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() from Should to Pester\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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!