1

Given a function that has validation for a parameter:

function Test-Validation {
    [CmdletBinding()]
    param (
        [Parameter()]
        [ValidateScript({
            # Add some validation that can throw.
            if (-not (Test-Path -Path $_ -PathType Container)) {
                throw "OutDir must be a folder path, not a file."
            }
            return $true
        })]
        [System.String]
        $Folder
    )
    Process {
        $Folder + " is a folder!"
    }
}

We should be able to check the error type and set that as the ExpectedType in a Pester Test.

Test-Validation -Folder C:\Temp\file.txt
Test-Validation : Cannot validate argument on parameter 'Folder'. OutDir must be a folder path, not a file.
At line:1 char:17
+ Test-Validation C:\Temp\file.txt
+                 ~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Test-Validation], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Test-Validation

$Error[0].Exception.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    True     ParameterBindingValidationException      System.Management.Automation.ParameterBindingException

However, when testing in Pester, the test fails because it cannot find the type.

$ShouldParams = @{
    Throw           = $true
    ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
    ExceptionType   = ([System.Management.Automation.ParameterBindingValidationException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should @ShouldParams

# Result
RuntimeException: Unable to find type [System.Management.Automation.ParameterBindingValidationException].

How can I fix this test so that I know I am not just catching any exception type?

Ash
  • 3,030
  • 3
  • 15
  • 33

2 Answers2

2

To generalize your own answer a bit:

  • Since working with the types of objects starting from a given instance isn't that common in PowerShell, the fact that an instance's type may be non-public isn't usually obvious, as long as the type derives from (is a subclass of) the expected public type.

    • While you can obtain an object's non-public type via .GetType(), you cannot refer to it via a type literal (e.g. [System.Management.Automation.ParameterBindingException]), such as for use in Pester tests or parameter declarations.
  • You can call .GetType().IsPublic on any given instance to check whether its type is public, and .GetType().BaseType to get that type's base type - though you may have to call the latter multiple types until you reach a type for which .IsPublic is $true - see the convenience function at the bottom.

In the case at hand .GetType().BaseType.FullName is sufficient to reach the public base type:

# Provoke a non-public [System.Management.Automation.ParameterBindingValidationException] 
# exception.
try { & { param([ValidateScript({ $false })] $foo)} bar } catch { $err = $_ }

# Output the full name of the exception type underlying the
# statement-terminating error that the failed validation reported:
$err.Exception.GetType().FullName

# It is only its *base* type that is public and therefore usable as a type
# literal ([...]), such as in a Pester test.
$err.Exception.GetType().BaseType.FullName

The above yields:

System.Management.Automation.ParameterBindingValidationException  # non-public
System.Management.Automation.ParameterBindingException            # public base type

Below is convenience function Get-PublicType, which, given any instance, reports the most derived type in the inheritance chain of the instance's type that is public (which may be the instance's type itself:

Sample call:

PS> Get-PublicType $err.Exception

PublicType                                             NonPublicDerivedType                                               Instance
----------                                             --------------------                                               --------
System.Management.Automation.ParameterBindingException {System.Management.Automation.ParameterBindingValidationException} System.Management.Automation.ParameterBindingValidationException: Cannot validate argument on par…

Get-PublicType source code:

function Get-PublicType {

  [CmdletBinding()]
  param(
    [Parameter(Mandatory, ValueFromPipeline)]
    $Instance
  )

  process {

    $type = $Instance.GetType()
    $nonPublicTypes = @()
  
    while (-not $type.IsPublic) {
      $nonPublicTypes += $type
      $type = $type.BaseType
    }
  
    # $type.FullName
    [pscustomobject] @{
      PublicType = $type
      NonPublicDerivedType = $nonPublicTypes
      Instance = $Instance
    }

  }

}
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Good additional information as always. I was going to add something similar for finding the closest public type earlier. Time constraints at work meant I had to move on, but thought the Q&A pair was useful enough if anyone came looking as Parameter Validation is not rare in PowerShell. I have a question, can you think of any reason why the [derived class](https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/utils/ParameterBinderExceptions.cs#L521) in the instance of the question was made internal? I see the constructors are _mainly_ internal for the base too. – Ash May 24 '21 at 19:14
  • 1
    Thanks, @Ash - your Q&A pair was definitely useful (I up-voted both). I can only _guess_ as to why the type at hand is non-public: Perhaps it is the general philosophy of only making those types public that end users need to interact with _as those types_. If the members of the _base_ class are sufficient from a public API perspective, then perhaps the derived types needn't be public. Conversely, there are so-called ["pubternal" types](https://www.reddit.com/r/dotnet/comments/jwzgl9/word_of_the_day_pubternal/) that are _conceptually_ internal, but are public for _technical_ reasons. – mklement0 May 24 '21 at 19:31
  • 1
    I would say that is a pretty good guess and makes sense in general. I was just initiallv thinking about a user writing their own validation attributes for a cmdlet and that they may want to throw the derived class, but then I think I recall that what ever exception you throw is the InnerException for the ParameterBindingValidationException when your attribute derives from ValidateArgumentsAttribute anyway. Appreciate the votes, I'm on my way to PowerShell bronze glory... – Ash May 24 '21 at 19:42
1

The reason why you cannot capture this type is because it is not a public class within [System.Management.Automation]. Instead you can set the -ExceptionType to the class it derives from [System.Management.Automation.ParameterBindingException] and your test will now pass with validation for the exception type thrown.

$ShouldParams = @{
    Throw           = $true
    ExpectedMessage = "Cannot validate argument on parameter 'OutDir'. OutDir must be a folder path, not a file."
    ExceptionType   = ([System.Management.Automation.ParameterBindingException])
}
{ Test-Validation -Folder C:\Temp\file.txt } | Should @ShouldParams
Ash
  • 3,030
  • 3
  • 15
  • 33