blade.ps1
<#
.SYNOPSIS Runs Blade tests in a file or directory. .DESCRIPTION The `blade.ps1` script, located in the root of the Blade module, is the script used to execute Blade tests. Given a path, it runs tests in any PowerShell script that begins with `Test-` (i.e. that matches the wildcard pattern `Test-*.ps1`. Tests are functions that that use the `Test` verb (i.e. whose name match the `Test-*` wildcard pattern). When executing tests, `blade.ps1` does the following: * Calls the `Start-TestFixture` function (if defined) * For each test, calls the `Start-Test` function (if defined), executes the test, then calls the `Stop-Test` (if defined). * Calls the `Stop-TestFixture` function (if defined) By default, Blade returns `Blade.TestResult` objects for each failed test. After running all tests, `blade.ps1` will write an error if any tests failed, then write a summary of the test run. The results of the last test run is available as a `Blade.RunResult` object in a global `$LastBladeResult` variable, e.g. > .\Blade\blade.ps1 .\Test Count Failures Errors Ignored Duration ----- -------- ------ ------- -------- 47 0 0 0 00:00:11.6870000 > $LastBladeResult | Format-List Count : 47 Name : Passed : {Test-ShouldDetectNoErrors, Test-ShouldThrowErrorIfNeedleMissingFromFile, Test-ShouldFailIfFileZeroBytes, Test-ShouldFailIfFileEmpty...} Failures : {} Errors : {} IgnoredCount : 0 Duration : 00:00:11.6870000 If you want Blade to return objects for each test, regarless if it failed or not, use the `-PassThru` switch. You can run specific test(s) by passing names to the `Test` parameter. Do not include the `Test-` verb/prefix. `blade.ps1` can also save test results as an NUnit XML report, so you can integrate test results into build servers and other reporting tools. Use the `XmlLogPath` parameter to specify the path to a log file. The file, and its parent directories, will be created if it doesn't exist. .LINK about_Blade .LINK about_Blade_Objects .EXAMPLE .\blade Test-MyScript.ps1 Will run all the tests in the `Test-MyScript.ps1` script. .EXAMPLE .\blade Test-MyScript.ps1 -Test MyTest Will run the `MyTest` test in the `Test-MyScript.ps1` test script. .EXAMPLE blade .\MyModule Will run all tests in the files which match the `Test-*.ps1` wildcard in the .\MyModule directory. .EXAMPLE blade .\MyModule -Recurse Will run all test in files which match the `Test-*.ps1` wildcard under the .\MyModule directory and its sub-directories. #> [CmdletBinding()] param( [Parameter(Mandatory=$true,Position=0)] [string[]] # The paths to search for tests. All files matching Test-*.ps1 will be run. $Path, [string] # The name of the tests being run. $Name, [string[]] # The individual test in the script to run. Defaults to all tests. Do not include the `Test-` verb/prefix. $Test, [string] # Path to the file where XML results should be saved. This file, and its parent directories, will be created if they don't exist. $XmlLogPath, [Switch] # Return objects for each test run, and a final summary object. $PassThru, [Switch] # Recurse through directories under `$Path` to find tests. $Recurse ) #Requires -Version 3 Set-StrictMode -Version 'Latest' & (Join-Path -Path $PSScriptRoot -ChildPath 'Import-Blade.ps1' -Resolve) function Get-FunctionsInFile($testScript) { Write-Debug -Message "Loading test script '$testScript'." $testScriptContent = Get-Content "$testScript" if( -not $testScriptContent ) { return @() } $errors = [Management.Automation.PSParseError[]] @() $tokens = [System.Management.Automation.PsParser]::Tokenize( $testScriptContent, [ref] $errors ) if( $errors -ne $null -and $errors.Count -gt 0 ) { Write-Error "Found $($errors.count) error(s) parsing '$testScript'." return } Write-Debug -Message "Found $($tokens.Count) tokens in '$testScript'." $functions = New-Object System.Collections.ArrayList $atFunction = $false for( $idx = 0; $idx -lt $tokens.Count; ++$idx ) { $token = $tokens[$idx] if( $token.Type -eq 'Keyword'-and $token.Content -eq 'Function' ) { $atFunction = $true } if( $atFunction -and $token.Type -eq 'CommandArgument' -and $token.Content -ne '' ) { Write-Debug -Message "Found function '$($token.Content).'" [void] $functions.Add( $token.Content ) $atFunction = $false } } return $functions.ToArray() } function Invoke-Test { <# .SYNOPSIS PRIVATE. Invokes a test from a fixture. .DESCRIPTION Internal function. Do not use. #> [CmdletBinding()] param( $fixture, $function ) Set-StrictMode -Version 'Latest' [Blade.TestResult]$testInfo = New-Object 'Blade.TestResult' $fixture,$function $Error.Clear() $testPassed = $false try { if( Test-path function:Start-Test ) { . Start-Test | ForEach-Object { $testInfo.Output.Add( $_ ) } } elseif( Test-Path function:SetUp ) { Write-Warning ('The SetUp function is obsolete and will be removed in a future version of Blade. Please use Start-Test instead.') . SetUp | ForEach-Object { $testInfo.Output.Add( $_ ) } } if( Test-Path function:$function ) { . $function | ForEach-Object { $testInfo.Output.Add( $_ ) } } $testPassed = $true } catch [Blade.AssertionException] { $ex = $_.Exception $testInfo.Completed( $ex ) } catch { $testInfo.Completed( $_ ) } finally { $tearDownResult = New-Object 'Blade.TestResult' $fixture,$function $tearDownFailed = $false try { if( Test-Path function:Stop-Test ) { . Stop-Test | ForEach-Object { $tearDownResult.Output.Add( $_ ) } } elseif( Test-Path -Path function:TearDown ) { Write-Warning ('The TearDown function is obsolete and will be removed in a future version of Blade. Please use Start-Test instead.') . TearDown | ForEach-Object { $tearDownResult.Output.Add( $_ ) } } $tearDownResult.Completed() } catch { $tearDownResult.Completed( $_ ) $tearDownFailed = $true } finally { if( $testPassed ) { $testInfo.Completed() } $flag = '! ' $result = 'FAILED' if( $testInfo.Passed ) { $flag = ' ' $result = 'Passed' } Write-Verbose -Message (' {0}{1} in {2:mm\:ss\.fff} [{3}]' -f $flag,$result,$testInfo.Duration,$function) $testInfo if( $tearDownFailed ) { $tearDownResult } } $Error.Clear() } } $getChildItemParams = @{ } if( $Recurse ) { $getChildItemParams.Recurse = $true } $testScripts = @( Get-ChildItem $Path Test-*.ps1 @getChildItemParams ) if( $testScripts -eq $null ) { $testScripts = @() } $Error.Clear() $testsIgnored = 0 $TestScript = $null $TestDir = $null $results = $null $testScripts | ForEach-Object { $testCase = $_ $TestScript = (Resolve-Path $testCase.FullName).Path $TestDir = Split-Path -Parent $testCase.FullName $testModuleName = [System.IO.Path]::GetFileNameWithoutExtension($testCase) $functions = Get-FunctionsInFile $testCase.FullName | Where-Object { $_ -match '^(Test|Ignore)-(.*)$' } | Where-Object { if( $PSBoundParameters.ContainsKey('Test') ) { return $Test | Where-Object { $Matches[2] -like $_ } } if( $Matches[1] -eq 'Ignore' ) { Write-Warning ("Skipping ignored test '{0}'." -f $_) $testsIgnored++ return $false } return $true } if( -not $functions ) { return } @('Start-TestFixture','Start-Test','Setup','TearDown','Stop-Test','Stop-TestFixture') | ForEach-Object { Join-Path -Path 'function:' -ChildPath $_ } | Where-Object { Test-Path -Path $_ } | Remove-Item Write-Verbose -Message ('[{0}]' -f $testCase.Name) . $testCase.FullName try { if( Test-Path -Path 'function:Start-TestFixture' ) { . Start-TestFixture | Out-String | Write-Debug } foreach( $function in $functions ) { if( -not (Test-Path -Path function:$function) ) { continue } Invoke-Test $testModuleName $function } if( Test-Path -Path function:Stop-TestFixture ) { try { . Stop-TestFixture | Out-String | Write-Debug } catch { Write-Error ("An error occured tearing down test fixture '{0}': {1}" -f $testCase.Name,$_) $result = New-Object 'Blade.TestResult' $testModuleName,'Stop-TestFixture' $result.Finished( $_ ) } } } finally { foreach( $function in $functions ) { if( $function -and (Test-Path function:$function) ) { Remove-Item function:\$function } } } } | Tee-Object -Variable 'results' | Where-Object { $PassThru -or -not $_.Passed } $Global:LastBladeResult = New-Object 'Blade.RunResult' $Name,([Blade.TestResult[]]$results), $testsIgnored if( $LastBladeResult.Errors -or $LastBladeResult.Failures ) { Write-Error $LastBladeResult.ToString() } if( $XmlLogPath ) { $LastBladeResult | Export-RunResultXml -FilePath $XmlLogPath } $LastBladeResult | Format-Table | Out-Host |