Unit Testing with Pester : Storing complex Mock objects in a JSON file

02/14/2017  |    6 minute read

When unit testing with Pester, mocking is pretty much unavoidable, especially for code related to infrastructure, configuration, or deployment. We don’t want our unit tests to touch files, databases, the registry, and not to mention the internet, do we ?

With Pester’s Mock function, we can isolate our code from this outside (hostile ?) world by faking commands and make them return whatever we want, even a custom object. Unfortunately, creating custom objects within the Mock is not ideal when dealing with complex Mock objects with nested properties.

Let’s see an example to understand why.

Let’s say we need to unit test a simple function (Get-ProcessModule) that lists all modules (DLLs) loaded in the process(es) specified by name :

Function Get-ProcessModule {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory)]
        [string]$Name        
    )
    $Processes = Get-Process -Name $Name

    If ( $Processes ) {
        Foreach ( $Process in $Processes ) {
            $LoadedModules = $Process.Modules
            Foreach ( $LoadedModule in $LoadedModules ) {
                $CustomProps = @{
                    'Name'= $LoadedModule.ModuleName
                    'Version'= $LoadedModule.ProductVersion
                    'PreRelease' = $LoadedModule.FileVersionInfo.IsPreRelease
                }
            $CustomObj = New-Object -TypeName psobject -Property $CustomProps
            $CustomObj
            }
        }
    }
}

Nothing fancy here, but notice that we are looking at a property named IsPreRelease which is nested in the FileVersionInfo property which itself is nested within the Modules property of our Process objects.

When unit testing this function, we don’t know which process(es) are running, and which DLLs they have loaded. And we don’t want to start new processes just for the sake of testing.
So, we will need to Mock Get-Process and return fake process objects with the properties we need, including the IsPreRelease nested property.

The script to unit test this function would look like this :

$ScriptPath = "$PSScriptRoot\Get-ProcessModule.ps1"
. $ScriptPath

Describe 'Get-ProcessModule' {
    Context 'There is 1 running process with the specified name' {
        Mock Get-Process {
            [PSCustomObject]@{
                Modules = @( @{
                    ModuleName = 'Module1FromProcess1'
                    ProductVersion = '1.0.0.1'
                    FileVersionInfo = @{
                        IsPreRelease = $False
                    }
                } );
            }
        }
        It 'Returns the correct module name' {
            (Get-ProcessModule -Name 'Any').Name |
            Should Be 'Module1FromProcess1'
        }
        It 'Returns the correct module version' {
            (Get-ProcessModule -Name 'Any').Version |
            Should Be '1.0.0.1'
        }
        It 'Returns the correct PreRelease value' {
            (Get-ProcessModule -Name 'Any').PreRelease |
            Should Be $False
        }
    }
}

While this does work, I’m not a big fan of cluttering the test file with 10 lines of code for every single Mock.
Imagine if we had a dozen (or more) different Mock objects to create, this would add up pretty quickly and make the test file difficult to follow.

I really like the idea that test scripts can act as an executable specification so I think we should strive to keep our test files as concise and readable as possible.
Granted, it’s no Gherkin, but this might be coming

Also, because these Mock objects are just fake objects with fake property values, they should be considered more as test data than code.
So, applying the separation of concerns principle, we should probably separate this data from the testing logic and store it in a distinct file.

Being a PowerShell kind of guy, my first choice was to use a standard PowerShell data file (.psd1). Let’s see how this works out :

@{
    Process1 =  [PSCustomObject]@{
        Modules = @( @{
            ModuleName = 'Module1FromProcess1'
            ProductVersion = '1.0.0.1'
            FileVersionInfo = @{
                IsPreRelease = $False
            }
        } );
    }
}

We have to specify the type [PSCustomObject], otherwise it would be a [hashtable] when imported back into PowerShell.
Unfortunately, Import-PowerShellDataFile doesn’t like that :

Import-PowerShellDataFile error

This is because to safely import data, Import-PowerShellDataFile works in RestrictedLanguage mode. And in this mode, casting to a [PSCustomObject] (to any type, for that matter) is forbidden.

We could use Invoke-Expression instead, but we’ve been told that Invoke-Expression is evil, so we should probably look for another option.

I heard that JSON is a nice and lightweight format to store data, so let’s try to use it to store our Mock objects.
Here is the solution I came up with to represent Mock objects as JSON :

{
    "Get-Process": [
        {
            "1ProcessWithMatchingName": {
                "Modules": {
                    "ModuleName": "Module1FromProcess1",
                    "ProductVersion": "1.0.0.1",
                    "FileVersionInfo": {
                        "IsPreRelease": false
                    }
                }
            }
        },
        {
            "2ProcessesWithMatchingName": [
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess1",
                        "ProductVersion": "1.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": false
                        }
                    }
                },
                {
                    "Modules": {
                        "ModuleName": "Module1FromProcess2",
                        "ProductVersion": "2.0.0.1",
                        "FileVersionInfo": {
                            "IsPreRelease": true
                        }
                    }
                }
            ]
        }
    ]
}

For true and false to be treated as proper boolean values, they have to be all lower case.

The data is organized hierarchically, as follow :

  • The top level is the name of the mocked command
  • The next level describes each scenario (or test case)
  • The inner level is the actual object(s) that we want the Mock to return

As we can see above, the second scenario (labelled “2ProcessesWithMatchingName”) returns an [array] of 2 objects. We could make it return 3, or more, if we wanted to.
We could also have multiple modules in some of our fake processes, but for illustration purposes, the above is enough.

We can import this data back into PowerShell with ConvertFrom-Json and explore the objects it contains, and their properties using what I call dot-browsing :

C:\> $JsonMockData = Get-Content -Path '.\MockObjects.json' -Raw
C:\> $Mocks = ConvertFrom-Json $JsonMockData
C:\> $2ndTestCase = $Mocks.'Get-Process'.'2ProcessesWithMatchingName'
C:\> $2ndTestCase.Modules

ModuleName          ProductVersion FileVersionInfo
----------          -------------- ---------------
Module1FromProcess1 1.0.0.1        @{IsPreRelease=False}
Module1FromProcess2 2.0.0.1        @{IsPreRelease=True}

C:\> $2ndTestCase.Modules.FileVersionInfo.IsPreRelease
False
True

Now, let’s see how we can use this in our tests :

$ScriptPath = "$PSScriptRoot\Get-ProcessModule.ps1"
. $ScriptPath

$JsonMockData = Get-Content -Path "$PSScriptRoot\MockObjects.json" -Raw
$Mocks = ConvertFrom-Json $JsonMockData

Describe 'Get-ProcessModule' {
    Context 'There is 1 running process with the specified name' {
        
        $ContextMock = $Mocks.'Get-Process'.'1ProcessWithMatchingName'
        Mock Get-Process { $ContextMock }
        It 'Returns the correct module name' {
            (Get-ProcessModule -Name 'Any').Name |
            Should Be $ContextMock.Modules.ModuleName
        }
        It 'Returns the correct module version' {
            (Get-ProcessModule -Name 'Any').Version |
            Should Be $ContextMock.Modules.ProductVersion
        }
        It 'Returns the correct PreRelease value' {
            (Get-ProcessModule -Name 'Any').PreRelease |
            Should Be $False
        }
    }
    Context 'There are 2 processes with the specified name' {
        
        $ContextMock = $Mocks.'Get-Process'.'2ProcessesWithMatchingName'
        Mock Get-Process { $ContextMock | Where-Object { $_ } }
        It 'Returns modules from both processes' {
            (Get-ProcessModule -Name 'Any').Count |
            Should Be 2
        }
    }
}

Within each Context block, we get the Mock object for a specific scenario that we have defined in our JSON data and store it into $ContextMock. Then, to define our Mock, we just specify that its return value is $ContextMock.

We can even use the $ContextMock variable to get the expected values for the Should assertions, like in the first 2 tests above.

You might be wondering why the hell I would filter $ContextMock with : Where-Object { $_ }, in the second Context block.
Well, this is because importing arrays from JSON to PowerShell has a tendency to add $Null items in the resulting array.

In this case, $ContextMock contained 3 objects : the 2 fake process objects, as expected, and a $Null element.
Why ? I have no idea, but I was able to get rid of it with the Where-Object statement above.

As we can see, it makes the tests cleaner and allows to define Mocks in an expressive way, so overall, I think this is a nice solution to manage complex Mock data.

That said, unit testing is still a relatively new topic in the PowerShell community, and I haven’t heard or read anything on best practices around test data. So I’m curious, how do you guys handle Mock objects and more generally, test data ? Do you have any tips or techniques ?

Comments

Nice post! Thank you!
I had some problems with scoping in my Pester tests when mocking functions in a script module.

If you try to mock a function call inside your script module you have to pass the name of your script module to “Mock”:

Mock Get-Process { $ContextMock } -ModuleName YourScriptModule

However, your $ContextMock object will not be the instance you set up outside of the Mock call because this is in another scope.

For being able to set up your mocks from a JSON file you can use InModuleScope. Then you have to load the mock objects and write your tests inside of the InModuleScope script block. Furthermore, you do not have to specify the module name with Mock.

InModuleScope -ModuleName YourScriptModule {
    
    $Mocks = Get-Content $PSScriptRoot\Mocks.json -Raw | ConvertFrom-Json
    Describe Get-ProcessModule {
        
        Context '1 running process' {
            $ContextMock = $Mocks.'Get-Process'.'1ProcessWithMatchingName'
            Mock Get-Process { $ContextMock }
            
            It '' {
                ...
            }
        }
    }

Hi Helmut,
Thanks for pointing that out.
The function in the article was in a simple script, but if the function under test is in a module, using InModuleScope prevents many scoping headaches !

Thanks for the great article! Personally if I have a particularly complex object to Mock, i’ve been temporarily modifying the code so that when it runs it exports that object to a file via Export-Clixml (then I’ve been editing the file to anonymize the data) and storing it in the Tests folder named <callingfunction><cmdletname>.mock.xml. Then obviously I just load the object for the mock with Import-Clixml. It does result however in the Tests folder becoming quite cluttered with XMLs, so I do like how your method puts them all together in a single file.

Leave a Comment

Your email address will not be published. Required fields are marked *

Loading...