In a previous post it was shown that the methods of PowerShell classes cannot determine the invoking line number and filename using $MyInvocation.ScriptLineNumber and $MyInvocation.ScriptName respectively (see PowerShell: $Myinvocation.ScriptLineNumber behaves incorrectly with Class Methods). Certain categories of methods make use of the invoking line numbers and filenames such as unit test methods (AreEqual, IsTrue, IsFalse, etc.) and logging methods (LogError, LogWarning, LogInfo, etc.). This post will demonstrate how to make use of PowerShell classes and methods in conjunction with functions. The functions will utilize $MyInvocation.ScriptLineNumber and $MyInvocation.ScriptName and pass the line number and filenames to the class methods.
The class, PsUnit, is based off of well known unit test frameworks such as NUnit, XUnit, and JUnit but clear PsUnit is a trivial subset of a full unit test suite. The PsUnit class exposes methods IsTrue, IsFalse, and AreEqual which each take a filename and line number parameter:
class PsUnit {
hidden static [void] IsTrue(
[bool] $shouldBeTrue,
[string] $filename,
[int] $lineNumber)
hidden static [void] IsFalse(
[bool] $shouldBeFalse,
[string] $filename,
[int] $lineNumber)
hidden static [void] AreEqual(
[object] $leftValue,
[object] $rightValue,
[string] $filename,
[int] $lineNumber)
}
The methods above are decorated with the hidden keyword. In PowerShell there is no private keyword like C++, Java, and C#. The hidden keyword means that features like IntelliSense do not detect methods prefixed by this keyword. The hidden keyword is as closest that PowerShell comes to the private keyword found in other languages. The long description of the keyword hidden is reviewed in Appendix A: The Hidden Keyword.
Each of the method signatures shown above is static, meaning that the method can be invoked without creating an instance of the PsUnit class. The following functions wrap these methods and pass in the filename and line numbers retrieved from $MyInvocation.ScriptName and $MyInvocation.ScriptLineNumber:
function IsTrue {
Param(
[Parameter(Mandatory=$true)]
[bool] $shouldBeTrue
)
[PsUnit]::IsTrue(
$shouldBeTrue,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
function IsFalse {
Param(
[Parameter(Mandatory=$true)]
[bool] $shouldBeFalse
)
[PsUnit]::IsFalse(
$shouldBeFalse,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
function AreEqual {
Param(
[object] $leftValue,
[object] $rightValue
)
[PsUnit]::AreEqual(
$leftValue,
$rightValue,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
The PsUnit class's methods IsTrue, IsFalse, and AreEqual all throw an exception if that value being tested contradicts the method name. The IsTrue method throws an exception if the value passed is $false. The AreEqual method throws an exception if the two values passed to the method are not equal.
The PsUnit class is implemented in its entirety is as follows including code to test code AreEqual, IsFalse, and IsTrue:
Set-StrictMode -Version 3.0
class PsUnit {
hidden static [string] $filenameLineNumberSeparator
hidden static [string] $filenameLineNumberStart
hidden static [string] $filenameLineNumberEnd
hidden static [string] $boolMismatchMessage
static PsUnit() {
[PsUnit]::filenameLineNumberStart = '('
[PsUnit]::filenameLineNumberEnd = ')'
[PsUnit]::filenameLineNumberSeparator = ':'
[PsUnit]::boolMismatchMessage = 'Test expected value of '
}
hidden static [string] CreateMessage(
[string] $message,
[string] $filename,
[int] $lineNumber) {
return "$message $([PsUnit]::filenameLineNumberStart)$filename" +
"$([PsUnit]::filenameLineNumberSeparator)$lineNumber$([PsUnit]::filenameLineNumberEnd)"
}
hidden static [void] IsTrue(
[bool] $shouldBeTrue,
[string] $filename,
[int] $lineNumber) {
if ($shouldBeTrue) {
return
}
throw [PsUnit]::CreateMessage(
"$([PsUnit]::boolMismatchMessage) $true",
$filename,
$lineNumber)
}
hidden static [void] IsFalse(
[bool] $shouldBeFalse,
[string] $filename,
[int] $lineNumber) {
if (!($shouldBeFalse)) {
return
}
throw [PsUnit]::CreateMessage(
"$([PsUnit]::boolMismatchMessage) $false",
$filename,
$lineNumber)
}
hidden static [void] AreEqual(
[object] $leftValue,
[object] $rightValue,
[string] $filename,
[int] $lineNumber) {
if (($null -eq $leftValue) -and ($null -eq $rightValue)) {
return
}
if ($null -eq $rightValue) {
throw [PsUnit]::CreateMessage(
"Test failed, assigned left value ($leftValue) cannot compared to right value of null",
$filename,
$lineNumber)
}
if ($null -eq $leftValue) {
throw [PsUnit]::CreateMessage(
"Test failed, assigned right value ($rightValue) cannot compared to left value of null",
$filename,
$lineNumber)
}
if ($leftValue.GetType() -ne $rightValue.GetType()) {
throw [PsUnit]::CreateMessage(
"Test failed type differ $($leftValue.GetType()) -ne $($rightValue.GetType())",
$filename,
$lineNumber)
}
if ($leftValue -eq $rightValue) {
return
}
throw [PsUnit]::CreateMessage(
"Test failed values not equal $leftValue -ne $rightValue",
$filename,
$lineNumber)
}
}
function IsTrue {
Param(
[Parameter(Mandatory=$true)]
[bool] $shouldBeTrue
)
[PsUnit]::IsTrue(
$shouldBeTrue,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
function IsFalse {
Param(
[Parameter(Mandatory=$true)]
[bool] $shouldBeFalse
)
[PsUnit]::IsFalse(
$shouldBeFalse,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
function AreEqual {
Param(
[object] $leftValue,
[object] $rightValue
)
[PsUnit]::AreEqual(
$leftValue,
$rightValue,
$MyInvocation.ScriptName,
$MyInvocation.ScriptLineNumber)
}
function Test-AreEqualUnequal {
Param(
[object] $leftValue,
[object] $rightValue
)
try {
AreEqual $leftValue $rightValue
}
catch {
return
}
throw "Exception should have been thrown ($($MyInvocation.ScriptName):$($MyInvocation.ScriptLineNumber))"
}
function Test-AreEqual {
try {
AreEqual $false $false
AreEqual $true $true
AreEqual $null $null
AreEqual 10 10
AreEqual 'xyZ' 'XYZ'
}
catch {
throw
}
Test-AreEqualUnequal 'xyZ' $null
Test-AreEqualUnequal $null 'xyZ'
Test-AreEqualUnequal 'qed' $null
Test-AreEqualUnequal $true $false
Test-AreEqualUnequal $false $true
Test-AreEqualUnequal 10 5
Test-AreEqualUnequal 'x' 5
}
function Test-Is-False {
[bool] $expectedException = $false
try {
IsFalse $false
IsFalse $($null -eq '')
$expectedException = $true
IsFalse $true
}
catch {
if ($expectedException) {
return
}
throw
}
}
function Test-Is-True {
[bool] $expectedException = $false
try {
IsTrue $true
IsTrue $('AbC' -eq 'aBC')
$expectedException = $true
IsTrue $false
}
catch {
if ($expectedException) {
return
}
throw
}
}
try {
Test-Are-Equal
Test-Is-False
Test-Is-True
Write-Host 'Success'
}
catch {
$errorRecord = $_
Write-Host "Error: $errorRecord.Exception"
Write-Host 'Failure'
}
Appendix A: The Hidden Keyword
The documentation for the hidden keyword can be found at: about_Hidden. The long description of hidden keyword is defined in "about_Hidden" as follows:
No comments :
Post a Comment