问题描述
问题
当一个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
生成,但它需要一点点拼接才能以与 dynamicparam
的 Should
块一起工作的方式生成它。详情请参阅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 测试成功。