将 Pester 结果导入 PowerShell
上周,Iron Scripter 网站上发布了 PowerShell 脚本挑战赛。这个想法是,当您运行 Pester 测试时,您可以将结果保存到特殊格式的 XML 文件中。
Invoke-Pester C:\scripts\sample.test.ps1 -OutputFile d:\temp\results.xml -OutputFormat JUnitXml我得到这个结果。
剧透警报
我编写了使用 JunitXML 格式重新创建 Pester 结果的函数。在某些时候修改该函数以能够处理两种 XML 格式会很好。我写的函数有一个 Path 参数。我可以指定一个值或通过管道将文件传递给命令。
Param(
[Parameter(position = 0, ValueFromPipeline, HelpMessage = "Specify the path to the JunitXML Pester test file.")]
[ValidateNotNullorEmpty()]
#verify the file exists and it is an XML file
[ValidateScript({
If ((Test-Path $_) -AND ($_.split('.')[-1] -eq 'XML' )) {
$True
}
else {
#display a custom validation error message
Throw "The file could not be found or wasn't an XML file."
}
})]
[Alias("pspath")]
[string]$Path
)
参数定义使用了一些验证测试。主要测试是确保文件存在并且以 XML 结尾。稍后我还有其他验证来测试它是否是 JunitXML 文件。是的,我知道从技术上讲我可以使用任何文件扩展名导出结果,但我假设 XML 加上我想演示这种技术。
参数中的另一个功能是我正在创建自己的自定义错误消息。当您使用 [ValidateScript()] 时,脚本块必须为您提供 True 或 False。就我而言,如果测试失败,我将抛出自定义错误消息。
PowerShell 7 可以更好地处理这个问题。
此时就需要解析 XML 文档了。
Process {
Write-Verbose "Processing $Path"
[xml]$doc = Get-Content -path $Path
我可以查看 XML 内部以确定这是否是一个 junit 文件。如果不是,我就会退出命令。
if ($doc.testsuites.noNamespaceSchemaLocation -notmatch "junit") {
Write-Warning "Could not import $Path. You must specify a Pester test result file using the JUnitXML format."
#abort
return
}
否则,我将解析文档以获取元数据信息,该信息组装在此处字符串中并使用 Write-Host 写入主机。事实上,整个函数使用 Write-Host 直接写入控制台。没有任何内容写入管道。尽管我稍后会向您展示一个选项。对于这个挑战,使用 Write-Host 是完全可以的。另一种选择是为 PowerShell 7 编写函数版本并使用 ANSI 转义进行着色。我会留到下次再写,或者你也可以写下来。
解析测试
棘手的部分是解析测试结果。我有这个 XML 可以使用。
<testcase name="Alpha.Should have a value over 1" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="0.465" />
<testcase name="Alpha.Should fail on a null value" status="Failed" classname="C:\work\sample.test.ps1" assertions="0" time="0.190">
<failure message="Expected $true, but got $false." />
</testcase>
<testcase name="Alpha.Should accept a mandatory Computername parameter" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="0.300" />
<testcase name="Alpha.Testing.Should have a value of 4" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="0.894" />
<testcase name="Alpha.Testing.Should run without error" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="1.150" />
<testcase name="Bravo.Should have a value less than 1" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="0.525" />
<testcase name="Bravo.Should export the gravitational constant" status="Skipped" classname="C:\work\sample.test.ps1" assertions="0" time="0.000">
<skipped message="" />
</testcase>
<testcase name="Bravo.Should run without errors" status="Passed" classname="C:\work\sample.test.ps1" assertions="0" time="2.357" />
<testcase name="Bravo.Should run with credentials" status="Pending" classname="C:\work\sample.test.ps1" assertions="0" time="0.000">
<skipped message="" />
</testcase>
Name 属性是关键项。第一部分,直到第一期,是描述块。在我的示例中,这是 Alpha 和 Bravo。句号之后的任何内容都是断言或 It 语句。除非,有一个像“测试”这样的中间词。这是一个 Pester 上下文元素。
我尝试了几种技巧来分解这段文字。最终我决定使用正则表达式模式。
[regex]$rx = "(?^[\w-_\s]+(?=\.))\.((?(?<=\.)\w+(?=\.))\.)?(?(?<=\.).*)"
该模式使用命名捕获来识别描述、上下文和断言组件。
想要了解有关 PowerShell 中正则表达式的更多信息?我为 Pluralsight 创建了一个关于该主题的完整课程。
我使用此模式将每个测试用例转换为自定义对象。
$tests = $doc.testsuites.testsuite.testcase | Group-Object -property Name
Write-Verbose "Processing $($tests.count) tests"
#turn each test into an object using a regex to split
$testobj = foreach ($test in $Tests) {
$m = $rx.Matches($test.name)
[pscustomobject]@{
Describe = $m.groups[2]
Context = $m.groups[3]
Assertion = $m.groups[4]
TestCase = $test.group
}
} #foreach test
我发现这更容易使用。现在我可以使用 Group-Object 和 ForEach 等命令以有序的方式处理结果,将结果写入基于主机的 east 测试。我最终创建了一个私有辅助函数 _parseTestCase,它将结果写入主机。当您查看代码时,您会发现我正在使用 Switch 语句来确定要使用的符号和前景色。
作为摘要,我将 Status 属性上的 TestCase 节点分组为哈希表。哈希表的键将是“失败”和“跳过”之类的内容。这使得我可以轻松获得需要显示的计数。
#write the summary information to the host in color
$testHash = $doc.testsuites.testsuite.testcase | Group-Object -property Status -AsHashTable -AsString
Write-Host "Tests completed in $(New-TimeSpan -seconds $doc.testsuites.time)" -ForegroundColor White
Write-Host "Tests Passed: $($doc.testsuites.tests), " -ForegroundColor White -NoNewline
Write-Host "Failed: $($TestHash["Failed"].count), " -ForegroundColor Red -NoNewline
Write-Host "Skipped: $($TestHash["Skipped"].count), " -ForegroundColor Yellow -NoNewline
Write-Host "Pending: $($TestHash["Pending"].count), " -ForegroundColor gray -NoNewline
Write-Host "Inconclusive: $($TestHash["Inconclusive"].count)" -ForegroundColor DarkGray
Import-TestResultFile.ps1:
#requires -version 5.1
<#
This command does not write anything to the pipeline.
It writes to the screen using Write-Host.
It requires a JunitXML formatted file from a Pester test.
This hasn't been tested with very complex Pester tests.
It assumes one or more Describe blocks with optional nested
Context sections.
#>
#TODO Verify handling of inconclusive results
#TODO Accept both types of XML formats
#TODO Add help documentation
#TODO Create a PS7 version that uses ANSI escapes for color
Function Import-TestResultFile {
[cmdletbinding()]
[alias("iptr")]
[outputtype("None")]
Param(
[Parameter(position = 0, ValueFromPipeline, HelpMessage = "Specify the path to the JunitXML Pester test file.")]
[ValidateNotNullorEmpty()]
#verify the file exists and it is an XML file
[ValidateScript({
If ((Test-Path $_) -AND ($_.split('.')[-1] -eq 'XML' )) {
$True
}
else {
#display a custom validation error message
Throw "The file could not be found or wasn't an XML file."
}
})]
[Alias("pspath")]
[string]$Path
)
Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
#a regular expression pattern that will be used to parse out the Describe block
[regex]$rx = "(?^[\w-_\s]+(?=\.))\.((?(?<=\.)\w+(?=\.))\.)?(?(?<=\.).*)"
#a private helper function for parse and display results
function _parseTestCase {
Param([object]$Result)
Switch ($result.status) {
"passed" {$sym = "+"; $fg = "green"}
"failed" {$sym = "-"; $fg = "red"}
"skipped" {$sym = "!"; $fg = "yellow"}
"pending" {$sym = "?"; $fg = "gray"}
} #switch
$line = " [{0}] {1} " -f $sym, $Assertion
Write-Host $line -foregroundcolor $fg -NoNewline
Write-Host ("{0}ms" -f $([double]$result.time * 1000)) -ForegroundColor darkGray
#display failure message
if ($sym -eq "-") {
$indent += " "
Write-Host "$indent$($result.failure.message)" -ForegroundColor Red
}
}
} #begin
Process {
Write-Verbose "Processing $Path"
[xml]$doc = Get-Content -path $Path
if ($doc.testsuites.noNamespaceSchemaLocation -notmatch "junit") {
Write-Warning "Could not import $Path. You must specify a Pester test result file using the JUnitXML format."
#abort
return
}
$pesterFile = $doc.testsuites.testsuite.name
$fileDate = (Get-Item -Path $Path).CreationTime.ToString().trim()
$computer = $doc.testsuites.testsuite.hostname
#display a header of test metadata information
$head = @"
$("*"*80)
Test File : $($pesterfile.PadRight(39," "))
Test Result : $((Convert-Path -path $Path).PadRight(39," "))
Test Date : $($filedate.PadRight(39," "))
Computername : $($computer.PadRight(39, " "))
$("*"*80)
"@
Write-Host $head -foreground cyan
$tests = $doc.testsuites.testsuite.testcase | Group-Object -property Name
Write-Verbose "Processing $($tests.count) tests"
#turn each test into an object using a regex to split
$testobj = foreach ($test in $Tests) {
$m = $rx.Matches($test.name)
[pscustomobject]@{
Describe = $m.groups[2]
Context = $m.groups[3]
Assertion = $m.groups[4]
TestCase = $test.group
}
} #foreach test
#group on the Describe Block
$describeGrp = $testObj | Group-Object -Property Describe
foreach ($grp in $describeGrp) {
$describe = $grp.Name
Write-Host "`n Describing $describe" -ForegroundColor green
#group on context if found
foreach ($value in $Grp.group) {
$Assertion = $value.Assertion
if ($value.Context -match "\w+") {
Write-Verbose "Detected context $($value.Context.value)"
Write-Verbose "Comparing to $currentctx"
if ($value.context.value -ne $currentCtx) {
Write-Host "`n Context $($value.Context)" -ForegroundColor green
$currentCtx = $Value.Context.value
}
foreach ($result in $value.testcase) {
_parseTestCase $result
} #foreach result
} #if context
else {
foreach ($result in $value.testcase) {
_parseTestCase $result
} #foreach result
} #else
} #foreach value
} #foreach grp
#write the summary information to the host in color
$testHash = $doc.testsuites.testsuite.testcase | Group-Object -property Status -AsHashTable -AsString
Write-Host "Tests completed in $(New-TimeSpan -seconds $doc.testsuites.time)" -ForegroundColor White
Write-Host "Tests Passed: $($doc.testsuites.tests), " -ForegroundColor White -NoNewline
Write-Host "Failed: $($TestHash["Failed"].count), " -ForegroundColor Red -NoNewline
Write-Host "Skipped: $($TestHash["Skipped"].count), " -ForegroundColor Yellow -NoNewline
Write-Host "Pending: $($TestHash["Pending"].count), " -ForegroundColor gray -NoNewline
Write-Host "Inconclusive: $($TestHash["Inconclusive"].count)" -ForegroundColor DarkGray
} #process
End {
Write-Verbose "Ending $($myInvocation.MyCommand)"
} #end
} 该函数有一个别名“iptr”。我没有使用很多不同的测试文件进行测试,所以可能有些事情我忽略了。但对于我编写的测试来说,这似乎有效。
写入信息
我还有一件事要告诉你。即使我的代码使用 Write-Host,也不会丢失所有内容。 Write-Host 也是 Write-Information 的包装器。这意味着当我像这样运行命令时(使用我的别名)
PS C:\>; iptr S:\sample.pester-results.xml -InformationVariable out我得到一个变量,$out。
PS C:\> $out[0..1] | select *
MessageData :
********************************************************************************
Test File : C:\work\sample.test.ps1
Test Result : C:\scripts\sample.pester-results.xml
Test Date : 4/13/2020 7:04:48 PM
Computername : DESKTOP1
********************************************************************************
Source : C:\scripts\Analyze-TestResult.ps1
TimeGenerated : 4/24/2020 12:57:33 PM
Tags : {PSHOST}
User : BOVINE320\Jeff
Computer : Bovine320
ProcessId : 17492
NativeThreadId : 26760
ManagedThreadId : 14
MessageData :
Describing Alpha
Source : C:\scripts\Analyze-TestResult.ps1
TimeGenerated : 4/24/2020 12:57:33 PM
Tags : {PSHOST}
User : BOVINE320\Jeff
Computer : Bovine320
ProcessId : 17492
NativeThreadId : 26760
ManagedThreadId : 14我可以用这些数据做任何我想做的事。我可以将所需内容重新导出到 json 或 xml 文件。或者将其发送到文本文件。如果不稍作调整,格式就不会相同。但同样,我会把这个练习留给你。
同时,如果您没有创建自己的挑战解决方案,我希望您尝试一下并让我知道您的想法。周末愉快。




