Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active March 4, 2021 19:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Jaykul/ca9f9481c804e28e5621871eac3066f9 to your computer and use it in GitHub Desktop.
Save Jaykul/ca9f9481c804e28e5621871eac3066f9 to your computer and use it in GitHub Desktop.
A better Parameter assertion for Pester Should
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