Last active
March 4, 2021 19:43
-
-
Save Jaykul/ca9f9481c804e28e5621871eac3066f9 to your computer and use it in GitHub Desktop.
A better Parameter assertion for Pester Should
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Assert-HasAParameter { | |
<# | |
.SYNOPSIS | |
Asserts that a command has the expected parameter (and supports aliases) | |
.EXAMPLE | |
Get-Command "Invoke-WebRequest" | Assert-HasAParameter Uri -Mandatory | |
.EXAMPLE | |
Get-Command "Invoke-WebRequest" | Should -HaveAMandatoryParameter Uri | |
This test passes, because it expected the parameter URI to exist and to | |
be mandatory. | |
.NOTES | |
The attribute [ArgumentCompleter] was added with PSv5. Previously this | |
assertion will not be able to use the -HasArgumentCompleter parameter | |
if the attribute does not exist. | |
LAST UPDATED 2019-08-30 to add: | |
-WithValueFromPipeline | |
-ByPropertyName | |
And the | Should -Not -HaveMoreParameters assertion | |
#> | |
# [PesterShould("HaveAParameter")] | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline)] | |
$ActualValue, | |
# The name of the parameter, usually passed as the first positional parameter | |
[Parameter(Position="0")] | |
[String]$ParameterName, | |
# The type of the parameter (tests type and PSTypeName constraint) | |
$OfType, | |
# An alias for the parameter | |
[String]$WithAlias, | |
# This is just meant to allow you to validate that there are validation attributes, like: "ValidateNotNull" or "AllowNull" | |
[String]$WithAttribute, | |
[Switch]$Mandatory, | |
# The name of a parameter set, to restrict the test | |
[string]$InParameterSet, | |
[Switch]$WithValueFromPipeline, | |
[Switch]$WithValueFromPipelineByPropertyName, | |
[Switch]$HasArgumentCompleter, | |
[Switch]$Not, | |
[String]$Because | |
) | |
process { | |
if ($null -eq $ActualValue -or $ActualValue -isnot [Management.Automation.CommandInfo]) { | |
throw "Input value must be a CommandInfo object. You can get one by calling Get-Command." | |
} | |
if ($null -eq $ParameterName) { | |
throw "The ParameterName can't be empty" | |
} | |
function Join-And ($Items, $Threshold = 2) { | |
if ($null -eq $items -or $items.count -lt $Threshold) { | |
$items -join ', ' | |
} else { | |
$c = $items.count | |
($items[0..($c - 2)] -join ', ') + ' and ' + $items[-1] | |
} | |
} | |
function Add-SpaceToNonEmptyString ([string]$Value) { | |
if ($Value) { | |
" $Value" | |
} | |
} | |
function Format-Because ([string] $Because) { | |
if ($null -eq $Because) { | |
return | |
} | |
$bcs = $Because.Trim() | |
if ([string]::IsNullOrEmpty($bcs)) { | |
return | |
} | |
" because $($bcs -replace 'because\s')," | |
} | |
if ($OfType -is [string]) { | |
# Parses type that is provided as a string in brackets (such as [int]) | |
$OfType = $OfType -replace '^\[(.*)\]$', '$1' | |
if ($parsedType = $OfType -as [Type]) { | |
$OfType = $parsedType | |
} | |
} | |
$buts = @() | |
$filters = @() | |
&(Get-Module Pester) { | |
param($Command, $Name) | |
$script:HaveAParameterTestedParameters[$Command] += @($Name) | |
} $ActualValue.Name $ParameterName | |
if ($InParameterSet) { | |
$Parameter = $ActualValue.ParameterSets.Where({ $_.Name -eq $InParameterSet }).Parameters.Where({ $_.Name -eq $ParameterName }) | |
[bool]$hasKey = [bool]$Parameter | |
} else { | |
$null = $ActualValue.Parameters # necessary for PSv2 | |
[bool]$hasKey = $ActualValue.Parameters.PSBase.ContainsKey($ParameterName) | |
} | |
$filters += "to$(if ($Not) {" not"}) have a parameter $ParameterName" | |
if (-not $Not -and -not $hasKey) { | |
$buts += "the parameter is missing" | |
} elseif ($Not -and -not $hasKey) { | |
return New-Object PSObject -Property @{ Succeeded = $true } | |
} elseif ($Not -and $hasKey -and -not ($Mandatory -or $OfType -or $DefaultValue -or $HasArgumentCompleter -or $WithAlias)) { | |
$buts += "the parameter exists" | |
} else { | |
if (!$InParameterSet) { | |
$Parameter = $ActualValue.Parameters[$ParameterName] | |
} | |
if ($Mandatory) { | |
$testMandatory = $Parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.Mandatory } | |
$filters += "which is$(if ($Not) {" not"}) mandatory" | |
if (-not $Not -and -not $testMandatory) { | |
$buts += "it wasn't mandatory" | |
} elseif ($Not -and $testMandatory) { | |
$buts += "it was mandatory" | |
} | |
} | |
if ($OfType) { | |
# This block is not using `Format-Nicely`, as in PSv2 the output differs. Eg: | |
# PS2> [System.DateTime] | |
# PS5> [datetime] | |
[type]$actualType = $Parameter.ParameterType | |
# OfType also passes if you have a matching PSTypeName attribute | |
$TypeAttribute = $Parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.PSTypeNameAttribute] } | |
$testType = ($OfType -eq $actualType) -or ($OfType -eq $TypeAttribute.PSTypeName) | |
$filters += "of type [$($OfType.FullName)]" | |
if (-not $Not -and -not $testType) { | |
$buts += "it was of type [$($actualType.FullName)]" | |
} elseif ($Not -and $testType) { | |
$buts += "it was of type [$($OfType.FullName)]" | |
} | |
} | |
if ($WithAlias) { | |
$testAlias = $Parameter.Aliases -Contains $WithAlias | |
$filters += "with the alias $WithAlias" | |
if (-not $Not -and -not $testAlias) { | |
$buts += "it is missing the alias $WithAlias" | |
} elseif ($Not -and $testAlias) { | |
$buts += "it does has the alias $WithAlias" | |
} | |
} | |
if ($WithAttribute) { | |
$testArgumentCompleter = $Parameter.Attributes.TypeId.Name.Contains('ValidateNotNullOrEmptyAttribute') | |
$filters += "with Attribute $WithAttribute" | |
if (-not $Not -and -not $testArgumentCompleter) { | |
$buts += "it has no $WithAttribute attribute" | |
} elseif ($Not -and $testArgumentCompleter) { | |
$buts += "it has $WithAttribute attribute" | |
} | |
} | |
if ($WithValueFromPipelineByPropertyName) { | |
$testPipeline = $Parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.ValueFromPipelineByPropertyName } | |
$filters += "with Attribute $WithValueFromPipelineByPropertyName" | |
if (-not $Not -and -not $testPipeline) { | |
$buts += "it has no $WithValueFromPipelineByPropertyName attribute" | |
} elseif ($Not -and $testPipeline) { | |
$buts += "it has $WithValueFromPipelineByPropertyName attribute" | |
} | |
} | |
if ($WithValueFromPipeline) { | |
$testPipeline = $Parameter.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.ValueFromPipeline } | |
$filters += "with Attribute $WithValueFromPipeline" | |
if (-not $Not -and -not $testPipeline) { | |
$buts += "it has no $WithValueFromPipeline attribute" | |
} elseif ($Not -and $testPipeline) { | |
$buts += "it has $WithValueFromPipeline attribute" | |
} | |
} | |
if ($HasArgumentCompleter) { | |
$testArgumentCompleter = $Parameter.Attributes | Where-Object { $_ -is [ArgumentCompleter] } | |
$filters += "with ArgumentCompletion" | |
if (-not $Not -and -not $testArgumentCompleter) { | |
$buts += "it has no ArgumentCompletion" | |
} elseif ($Not -and $testArgumentCompleter) { | |
$buts += "it has ArgumentCompletion" | |
} | |
} | |
} | |
if ($buts.Count -ne 0) { | |
$filter = Add-SpaceToNonEmptyString ( Join-And $filters -Threshold 3 ) | |
$but = Join-And $buts | |
$failureMessage = "Expected command $($ActualValue.Name)$filter,$(Format-Because $Because) but $but." | |
return New-Object PSObject -Property @{ | |
Succeeded = $false | |
FailureMessage = $failureMessage | |
} | |
} else { | |
return New-Object PSObject -Property @{ Succeeded = $true } | |
} | |
} | |
} | |
try { | |
$HaveAParameter = ${function:Assert-HasAParameter} | |
# We clear the tested parameters cache every time this file is dotsourced, but not otherwise ... | |
InModuleScope Pester { $script:HaveAParameterTestedParameters = @{ } } | |
Add-AssertionOperator HaveAParameter { | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline)] | |
$ActualValue, | |
# The name of the parameter, usually passed as the first positional parameter | |
[Parameter(Position = "0")] | |
[String]$ParameterName, | |
# The type of the parameter (tests type and PSTypeName constraint) | |
$OfType, | |
# An alias for the parameter | |
[String]$WithAlias, | |
# This is just meant to allow you to validate that there are validation attributes, like: "ValidateNotNull" or "AllowNull" | |
[String]$WithAttribute, | |
# The name of a parameter set, to restrict the test | |
[string]$InParameterSet, | |
[Switch]$WithValueFromPipeline, | |
[Switch]$WithValueFromPipelineByPropertyName, | |
[Switch]$HasArgumentCompleter, | |
[Switch]$Negate, | |
[String]$Because | |
) | |
$null = $PSBoundParameters.Remove("Negate") | |
& $HaveAParameter @PSBoundParameters -Not:$Negate | |
}.GetNewClosure() | |
Add-AssertionOperator HaveAMandatoryParameter { | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline)] | |
$ActualValue, | |
# The name of the parameter, usually passed as the first positional parameter | |
[Parameter(Position = "0")] | |
[String]$ParameterName, | |
# The type of the parameter (tests type and PSTypeName constraint) | |
$OfType, | |
# An alias for the parameter | |
[String]$WithAlias, | |
# This is just meant to allow you to validate that there are validation attributes, like: "ValidateNotNull" or "AllowNull" | |
[String]$WithAttribute, | |
# The name of a parameter set, to restrict the test | |
[string]$InParameterSet, | |
[Switch]$WithValueFromPipeline, | |
[Switch]$WithValueFromPipelineByPropertyName, | |
[Switch]$HasArgumentCompleter, | |
[Switch]$Negate, | |
[String]$Because | |
) | |
$null = $PSBoundParameters.Remove("Negate") | |
& $HaveAParameter @PSBoundParameters -Not:$Negate -Mandatory | |
}.GetNewClosure() | |
Add-AssertionOperator HaveMoreParameters { | |
[CmdletBinding()] | |
param( | |
[Parameter(ValueFromPipeline)] | |
$ActualValue, | |
[Switch]$Negate, | |
[String]$Because | |
) | |
$ActualParameters = $ActualValue.Parameters.Values.Name.Where{ | |
$_ -notin [System.Management.Automation.Cmdlet]::CommonParameters | |
} | |
$TestedParameters = @( | |
&(Get-Module Pester) { | |
param($Command, $Parameters) | |
# return the tested parameters in parameter order | |
$Parameters.Where{ $_ -in $Script:HaveAParameterTestedParameters[$Command] } | |
} $ActualValue.Name $ActualParameters) | |
$Missed = @($ActualParameters.Where{$_ -notin $TestedParameters}) | |
if ($Negate) { | |
if ($Missed) { | |
$failureMessage = "Expected command $($ActualValue.Name) to not have any parameters" | |
if ($TestedParameters) { | |
$failureMessage += " except '$($TestedParameters -join "', '" -replace ', ([^,]+)$', ', and $1')'" | |
} | |
if ($bcs = $Because.Trim()) { | |
$failureMessage += " because $($bcs -replace 'because\s')," | |
} | |
return New-Object PSObject -Property @{ | |
Succeeded = $false | |
FailureMessage = $failureMessage += " but it has '$($missed -join "', '" -replace ', ([^,]+)$', ', and $1')'" | |
} | |
} | |
} else { | |
if (!$Missed) { | |
$failureMessage = "Expected command $($ActualValue.Name) to have parameters" | |
if ($TestedParameters) { | |
$failureMessage += " besides '$($TestedParameters -join "', '" -replace ', ([^,]+)$', ', and $1')'" | |
} | |
if ($bcs = $Because.Trim()) { | |
$failureMessage += " because $($bcs -replace 'because\s')," | |
} | |
return New-Object PSObject -Property @{ | |
Succeeded = $false | |
FailureMessage = $failureMessage += " but it doesn't!" | |
} | |
} | |
} | |
return New-Object PSObject -Property @{ Succeeded = $true } | |
}.GetNewClosure() | |
} catch { | |
if ($_.Exception.Message -notmatch "Assertion operator name 'Have.*Parameters?' has been added multiple times.") { | |
throw $_ | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment