网站搜索

将 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 文件。或者将其发送到文本文件。如果不稍作调整,格式就不会相同。但同样,我会把这个练习留给你。

同时,如果您没有创建自己的挑战解决方案,我希望您尝试一下并让我知道您的想法。周末愉快。

相关文章