当纠缠测试用例失败显示哈希表的内容

问题描述

问题

一个Hashtable作为输入为Should,纠缠仅输出类型名,而不是内容

Describe 'test' {
    It 'test case' {
        $ht = @{ foo = 21; bar = 42 }
        $ht | Should -BeNullOrEmpty
    }
}

输出

Expected $null or empty,but got @(System.Collections.Hashtable).

预期输出这样的:

Expected $null or empty,but got @{ foo = 21; bar = 42 }.

原因

综观Pester source,测试输入由私有函数格式化Format-Nicely,这只是投射到String如果值类型是Hashtable。这归结为呼叫Hashtable::ToString(),这只是输出类型名称

解决方法

作为一种变通方法我目前导出从Hashtable,它覆盖ToString方法的类。通过输入到前Should,我将它转换为这个自定义类。这使得纠缠调用我重写ToString方法格式化的测试结果时。

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
    }
}

现在纠缠输出Hashtable在JSON格式的内容,这是很好的对我来说足够。

问题

是否有定制的纠缠输出更优雅的方式Hashtable,其不需要我改变每个测试用例的代码

解决方法

有点像黑客,通过定义同名的全局别名来覆盖 Pester 的私有 Format-Nicely cmdlet。

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
}

这使我们能够像往常一样编写测试用例:

Describe 'test' {
    It 'logs hashtable content' {
        $ht = @{ foo = 21; bar = 42 }
        $ht | Should -BeNullOrEmpty
    }   

    It 'logs other types regularly' {
        $true | Should -Be $false 
    }
}

第一个测试用例的日志:

Expected $null or empty,but got @({
 "foo": 21,"bar": 42
}).

第二个测试用例的日志:

Expected $false,but got $true.
,

my previous answer 更简洁(虽然更冗长)的方法是为 Should 编写包装函数。

这样的包装器可以使用 System.Management.Automation.ProxyCommand 生成,但它需要一点点拼接才能以与 dynamicparamShould 块一起工作的方式生成它。详情请参阅this answer

包装器 process 块被修改为将当前管道对象转换为自定义 Hashtable 派生类,该类覆盖 .ToString() 方法,然后将其传递给 process原始 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
        }        
    }
}

要通过包装器覆盖 Pesters Should,请定义一个全局别名,如下所示:

Set-Alias Should MyShould -Force -Scope Global

并恢复原来的Should

Remove-Alias MyShould -Scope Global

注意事项:

  • 我还将 GetCommand() 的参数从 Should 更改为 Pester\Should 以避免由于别名引起的递归。不过不确定这是否真的有必要。
  • 需要最新版本的 Pester。使用 Pester 5.0.4 失败,但使用 Pester 5.1.1 测试成功。