Pester icon indicating copy to clipboard operation
Pester copied to clipboard

Mocking with `-ModuleName` fails when mocked function has a parameterized type internal to the module.

Open LethiferousMoose opened this issue 3 years ago • 5 comments

Checklist

What is the issue?

I was attempting to create a pester test for a function that calls an internal module function that has a List parameter of an internal module class. Note: This works fine if the parameter is NOT a list, i.e. single object or array of objects.

Starting discovery in 1 files.
Discovery found 1 tests in 62ms.
Running tests.
[-] Describe Will not work failed
 ParseException: At line:3 char:66
 + … [System.Collections.Generic.List`1[[Parameterized, PowerShell Class A …
 +                                                                ~
 Missing ] at end of type token.
 At line:3 char:66
 + … [System.Collections.Generic.List`1[[Parameterized, PowerShell Class A …
 +                                                                ~
 Missing ] at end of attribute or type literal.
 At line:3 char:67
 + … System.Collections.Generic.List`1[[Parameterized, PowerShell Class As …
 +                                                                ~
 Parameter declarations are a comma-separated list of variable names with optional initializer expressions.
 At line:3 char:67
 + … System.Collections.Generic.List`1[[Parameterized, PowerShell Class As …
 +                                                                ~
 Missing ')' in function parameter list.
 MethodInvocationException: Exception calling "Create" with "1" argument(s): "At line:3 char:66
 + … [System.Collections.Generic.List`1[[Parameterized, PowerShell Class A …
 +                                                                ~
 Missing ] at end of type token.
 At line:3 char:66
 + … [System.Collections.Generic.List`1[[Parameterized, PowerShell Class A …
 +                                                                ~
 Missing ] at end of attribute or type literal.
 At line:3 char:67
 + … System.Collections.Generic.List`1[[Parameterized, PowerShell Class As …
 +                                                                ~
 Parameter declarations are a comma-separated list of variable names with optional initializer expressions.
 At line:3 char:67
 + … System.Collections.Generic.List`1[[Parameterized, PowerShell Class As …
 +                                                                ~
 Missing ')' in function parameter list."
Tests completed in 198ms
Tests Passed: 0, Failed: 1, Skipped: 0 NotRun: 0
BeforeAll \ AfterAll failed: 1
  - Will not work

Expected Behavior

The mock should be created without runtime errors.

Steps To Reproduce

PSTest.psm1

class Parameterized {
    [string] $RandomData
}

function IDoStuffWithLists {
    param (
        [System.Collections.Generic.List[Parameterized]] $Things
    )
}

function IDoStuffWithArrays {
    param (
        [Parameterized[]] $Things
    )
}

function IDoStuff {
    param (
        [Parameterized] $Things
    )
}

function ICallIDoStuff {
    IDoStuffWithLists
    IDoStuffWithArrays
    IDoStuff
}

Export-ModuleMember -Function ICallIDoStuff

ICalIDoStuff.Tests.ps1

BeforeAll {
    Import-Module PSTest -Force
}

Describe 'This is inconsistent' {
    BeforeAll {
        Mock IDoStuff -ModuleName 'PSTest'           # Works
        Mock IDoStuffWithArrays -ModuleName 'PSTest' # Works
        Mock IDoStuffWithLists -ModuleName 'PSTest'  # Fails
    }

    It 'Call function' {
        ICallIDoStuff
    }
}

Describe your environment

Pester version     : 5.3.3 C:\Users\...\Documents\PowerShell\Modules\Pester\5.3.3\Pester.psm1
PowerShell version : 7.2.6
OS version         : Microsoft Windows NT 10.0.19043.0

Possible Solution?

No response

LethiferousMoose avatar Aug 12 '22 03:08 LethiferousMoose

Thanks for the report! Looks like this is because PowerShell creates a special FullName for classes created in modules especially. Simpler repro:

New-Module -Name abc {
    class SomeClass {
        [string] $RandomData
    }

    function someFunc {
        param(
            [SomeClass] $Param1
        )
    }
} | Import-Module

(& (Get-Module abc) { [someclass] }).FullName
<1e6b41c3>.SomeClass

Pester uses PowerShell's ProxyCommand API to create the mock's param-block. It uses the type's fullname which in these scenarios includes invalid characters (numbers, special characters etc) for the parameter-type.

[System.Management.Automation.ProxyCommand]::GetParamBlock((Get-Command someFunc))

    [<941113fe>.SomeClass]
    ${Param1}

In your sample the equivalent is:

$metadata.Parameters.Things.ParameterType.FullName
System.Collections.Generic.List`1[[Parameterized, PowerShell Class Assembly, Version=1.0.0.5, Culture=neutral, PublicKeyToken=null]]

# which generates this param-block:
[System.Management.Automation.ProxyCommand]::GetParamBlock($metadata)

[System.Collections.Generic.List`1[[Parameterized, PowerShell Class Assembly, Version=1.0.0.5, Culture=neutral, PublicKeyToken=null]], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]
    ${Things}

fflaten avatar Aug 12 '22 22:08 fflaten

Not sure which part of this is considered a bug vs limitation, but I'll create an issue in PowerShell-repo. Using a generic type (List) didn't make this easier.

In my sample they could've simply used Name for the type, which would be SomeClass, but for your list that would be List`1 which wouldn't work.

We can try to rewrite this in Pester, just need to get the detection right so we don't modify and break anything else.

fflaten avatar Aug 12 '22 22:08 fflaten

@fflaten thanks for the response, I switched over to using arrays, this is part of old code I am cleaning up and they no longer need to be mutable and it gets me past this issue. But I still thought it was worth mentioning as a limitation/bug.

LethiferousMoose avatar Aug 12 '22 22:08 LethiferousMoose

Absolutely. 🙂 The issue isn't related to the use of List, but classes in general, so fixing this might be valuable for others too. Ex. this will currently fail:

Describe 'PowerShell classes == pain' {
    BeforeAll {
        New-Module -Name abc {
            class SomeClass {
                [string] $RandomData
            }
            function someFunc {
                param(
                    [SomeClass] $Param1
                )
            }

            function publicFunc {
                someFunc
            }
        } | Import-Module

        Mock someFunc # fails here
    }

    It 'Call function' {
        publicFunc
    }
}

# output
Starting discovery in 1 files.
Discovery found 1 tests in 205ms.
Running tests.
[-] Describe PowerShell classes == pain failed
 ParseException: At line:3 char:6
 +     [<a6061cc7>.<9fceeca4>.<3e98a71c>.SomeClass]
 +      ~
 Missing type name after '['.
 At line:2 char:12
 +     param (
 +            ~
 Missing ')' in function parameter list.
....lots of errors...

fflaten avatar Aug 12 '22 22:08 fflaten

Btw. I forgot to mention a workaround for now, try -RemoveParameterType <paramName>. Ex Mock IDoStuffWithLists -ModuleName 'PSTest' -RemoveParameterType Things

fflaten avatar Aug 13 '22 15:08 fflaten