Pester 测试用例失败时显示哈希表的内容
Show content of hashtable when Pester test case fails
问题
当 Hashtable
用作 Should
的输入时,Pester 仅输出类型名称而不是内容:
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
格式化,如果值类型为 Hashtable
,它只是转换为 String
。这归结为调用 Hashtable::ToString()
,它只输出类型名。
解决方法
作为一种解决方法,我目前从 Hashtable
中派生了一个 class,它覆盖了 ToString
方法。在将输入传递给 Should
之前,我将其转换为这个自定义 class。这使得 Pester 在格式化测试结果时调用我重写的 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
}
}
现在 Pester 以 JSON 格式输出 Hashtable
内容,这对我来说已经足够了。
问题
有没有更优雅的方法来自定义 Hashtable
的 Pester 输出,不需要我更改每个测试用例的代码?
有点 hack,通过定义同名的全局别名来覆盖 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.
比 更简洁(尽管更冗长)的方法是为 Should
.
编写包装函数
这样的包装器可以使用 System.Management.Automation.ProxyCommand
生成,但需要一些拼接才能以与 Should
的 dynamicparam
块一起使用的方式生成它。有关详细信息,请参阅 。
wrappers process
块被修改为将当前管道对象转换为自定义 Hashtable
派生的 class,它覆盖 .ToString()
方法,然后再将其传递给原始 Should
cmdlet 的 process
块。
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 测试成功。
问题
当 Hashtable
用作 Should
的输入时,Pester 仅输出类型名称而不是内容:
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
格式化,如果值类型为 Hashtable
,它只是转换为 String
。这归结为调用 Hashtable::ToString()
,它只输出类型名。
解决方法
作为一种解决方法,我目前从 Hashtable
中派生了一个 class,它覆盖了 ToString
方法。在将输入传递给 Should
之前,我将其转换为这个自定义 class。这使得 Pester 在格式化测试结果时调用我重写的 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
}
}
现在 Pester 以 JSON 格式输出 Hashtable
内容,这对我来说已经足够了。
问题
有没有更优雅的方法来自定义 Hashtable
的 Pester 输出,不需要我更改每个测试用例的代码?
有点 hack,通过定义同名的全局别名来覆盖 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.
比 Should
.
这样的包装器可以使用 System.Management.Automation.ProxyCommand
生成,但需要一些拼接才能以与 Should
的 dynamicparam
块一起使用的方式生成它。有关详细信息,请参阅
wrappers process
块被修改为将当前管道对象转换为自定义 Hashtable
派生的 class,它覆盖 .ToString()
方法,然后再将其传递给原始 Should
cmdlet 的 process
块。
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 测试成功。