Search code examples
powershellpester

How to use Pester New-MockObject with class from within a module


Synopsis: When trying to calling New-MockObject -Type 'Config' where Config is a class defined in another psm1 file, I get the following error PSInvalidCastException: Cannot convert the "Config" value of type "System.String" to type "System.Type"

Using Powershell 5.1 with Pester V5.6.1

I have put together the following simple sample to demonstrate the problem

  • Client depends on Config
  • Client.Tests depends on Config & Client
  • Not sure if this is important, but Client has a using statement for Config, and therefore transitively, Client.Tests has 2 using calls to Config, 1 in Client.Tests and another in Client

All files are in the same directory

Config.psm1

class Config 
{
    [ValidateLength(1, 1000)]
    [string] $A
    
    [ValidateLength(1, 1000)]
    [string] $B    

     Config( [string] $pathToJson) 
     {
        try 
        {
            $jsonContent = Get-Content -Path $pathToJson -Raw | ConvertFrom-Json

            $this.A = $jsonContent.A
            $this.B = $jsonContent.B
        } 
        catch 
        {
            Write-Warning $_.Exception.Message
            Write-Warning "Check json file for missing value(s)"
            throw "Error loading configuration from file: $($_.Exception.Message)"
        }
    }
}

Client.psm1

using module .\Config.psm1

class Client
{
    [Config] $config

    Client([Config] $config) 
    {
        $this.config = $config
    }


    [string] JoinAandB()
    {        
        return $this.config.A + $this.config.B
    }
}

Client.Tests.ps1

using module .\Config.psm1
using module .\Client.psm1

Describe "SomeClassThatUsesAzureDevOpsAgentConfig" {    
 
    It "JoinAandB__A_and_B_Are_Populated__ReturnsAB" {
        # Arrange
        $mockConfig = New-MockObject -Type  'Config' -Properties @{A = "foo"; B = "Bar"}
        $systemUnderTest = [Client]::new($mockConfig)
        $expectedResult = "fooBar"

        # Act
        $actualResults = $systemUnderTest.JoinAandB()

        # Assert
        $actualResults | Should -Be $expectedResult
    }
}

I'm not including the JSON since its not relevant, as I am trying to mock the config class, so it should not be needed.

The Error message I get when I run Invoke-Pester .\Client.Tests.ps1

Starting discovery in 1 files.
Discovery found 1 tests in 16ms.
Running tests.
[-] SomeClassThatUsesAzureDevOpsAgentConfig.JoinAandB__A_and_B_Are_Populated__ReturnsAB 5ms (5ms|1ms)
 PSInvalidCastException: Cannot convert the "Config" value of type "System.String" to type "System.Type".
 ArgumentTransformationMetadataException: Cannot convert the "Config" value of type "System.String" to type "System.Type".
 ParameterBindingArgumentTransformationException: Cannot process argument transformation on parameter 'Type'. Cannot convert the "Config" value of type "System.String" to type "System.Type".
 at <ScriptBlock>, C:\Users\dgleason\source\repos\DevOpsTeam\IaC\installs\azure_devops_agent_configuration\Client.Tests.ps1:9
Tests completed in 77ms
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

I have gone ahead and opened an issue in Pester Github page as well - https://github.com/pester/Pester/issues/2564


Solution

  • The following fixes your problem, but I'm not sure why:

    • Instead of passing the name of the target type (class) to -Type, as a string ('Config'), pass a [Type] instance representing it; note the need to enclose the type literal ([...]) in (...) in order to pass it as such as a command argument:
    $mockConfig = New-MockObject -Type ([Config]) -Properties @{A = "foo"; B = "Bar"}
    

    Similarly, [Client]::new($mockConfig) only works because you're using the intrinsic new method to call the constructor of the Client class; the - normally equivalent - New-Object Client $mockConfig statement would not (that said, using ::new() is generally preferable).

    Your symptoms are connected to code executing inside the It statement, specifically.