diff --git a/src/functions/assertions/BeOfType.ps1 b/src/functions/assertions/BeOfType.ps1 index 8d03d0a22..c5dfd7704 100644 --- a/src/functions/assertions/BeOfType.ps1 +++ b/src/functions/assertions/BeOfType.ps1 @@ -37,7 +37,24 @@ function Should-BeOfTypeAssertion($ActualValue, $ExpectedType, [switch] $Negate, $trimmedType = $ExpectedType -replace '^\[(.*)\]$', '$1' $parsedType = $trimmedType -as [Type] if ($null -eq $parsedType) { - throw [ArgumentException]"Could not find type [$trimmedType]. Make sure that the assembly that contains that type is loaded." + # PowerShell classes loaded via dot-sourcing may not be visible to + # the module scope. Try to resolve from the actual value's type (#2701). + if ($null -ne $ActualValue) { + $actualType = $ActualValue.GetType() + # Walk the inheritance chain to find a matching type name + $t = $actualType + while ($null -ne $t) { + if ($t.Name -eq $trimmedType -or $t.FullName -eq $trimmedType) { + $parsedType = $t + break + } + $t = $t.BaseType + } + } + + if ($null -eq $parsedType) { + throw [ArgumentException]"Could not find type [$trimmedType]. Make sure that the assembly that contains that type is loaded." + } } $ExpectedType = $parsedType diff --git a/tst/functions/assertions/BeOfType.Tests.ps1 b/tst/functions/assertions/BeOfType.Tests.ps1 index 418d57b0e..a59e5d675 100644 --- a/tst/functions/assertions/BeOfType.Tests.ps1 +++ b/tst/functions/assertions/BeOfType.Tests.ps1 @@ -44,4 +44,52 @@ InPesterModuleScope { $err.Exception.Message | Verify-Equal 'Expected the value to not have type [int] or any of its subtypes, because reason, but got 1 with type [int].' } } + + Describe "Should -BeOfType with types not visible in module scope" { + # PowerShell classes defined via dot-sourcing in BeforeAll are not visible + # to the Pester module scope. The fallback resolves the type from the + # actual value's inheritance chain by comparing type names. + + It "resolves type from actual value when -as [Type] fails" { + # Create a type that is not loadable by name in this scope + # by using the actual object's type hierarchy + $obj = [System.IO.MemoryStream]::new() + try { + # These will resolve via -as [Type] normally, but also verify + # the assertion logic works for both Name and FullName + $obj | Should -BeOfType 'MemoryStream' + $obj | Should -BeOfType 'System.IO.MemoryStream' + # Base type matching + $obj | Should -BeOfType 'Stream' + $obj | Should -BeOfType 'System.IO.Stream' + } + finally { + $obj.Dispose() + } + } + + It "resolves type by walking actual value's inheritance chain" { + # When -as [Type] fails (e.g. PS classes not visible to module scope), + # the fallback walks the actual value's type hierarchy by Name/FullName. + # We test this by calling the assertion function directly with an object + # whose type is known but using its Name string (which -as [Type] resolves). + # The real scenario (PS class not visible) can't be unit-tested without + # nested Invoke-Pester, but we verify the hierarchy walk works correctly. + $obj = [System.IO.MemoryStream]::new() + try { + # MemoryStream inherits from Stream — verify base type matching works + $obj | Should -BeOfType 'Stream' + $obj | Should -BeOfType 'System.IO.Stream' + $obj | Should -BeOfType 'MarshalByRefObject' + } + finally { + $obj.Dispose() + } + } + + It "throws ArgumentException when actual is `$null and type is not resolvable" { + $err = { $null | Should -BeOfType 'SomeNonExistentClass' } | Verify-Throw + $err.Exception | Verify-Type ([ArgumentException]) + } + } }