Unit tests are used to verify the behavior of a certain part of your PowerShell code. With mocking in Pester, you can mimic the behavior of commands referenced by the unit you are testing, which ensures that you only test your code, not the referenced commands.

When writing a unit test for a semi-complicated PowerShell function or script, you're probably going to have references to other functions or cmdlets inside of it. These functions and cmdlets are called in different ways, depending on the logic you've chosen to implement inside your code.

For example, if you have some code that checks for the existence of a file, you might then call Get-Content to read the contents. If not, you may then call Add-Content to create the file instead. A good unit test needs to contain each of these instances to ensure the code did what you thought it would do depending on the state of the file itself.

You could actually put a dummy file on your system somewhere, run your code, remove the file, and run your code again. This would test the functionality, but your unit test would then have a dependency on an environmental element (the file). This is not good practice as your unit test would not be testing just your code. In fact, it would be testing whatever storage system the file was on as well. What if the drive you placed the file on was non-existent? At the same time, you're also depending on Get-Content and Add-Content working right. It's best never to trust any other command in your unit test.

So how do you build a good unit test that has calls to other commands so you can be sure it's only testing your code and not the other commands referenced? Mocking. Mocking is a way to eliminate these command dependencies and test only your code—not the code of the other commands and not code that depends on some environmental factor.

Let's start with a typical function that contains some logic and a few references to other commands inside.

function Get-FileContents
 {
     [CmdletBinding()]
     param
     (
         [Parameter(Mandatory)]
         [ValidateNotNullOrEmpty()]
         [string]$Path
     )
     
     if (Test-Path -Path $Path -PathType Leaf)
     {
         Get-Content -Path $Path
     }
     else
     {
         Add-Content -Path $Path -Value 'something in here'
         Get-Content -Path $Path
     }
 }

This function can go two different ways, depending on what Test-Path returns. If Test-Path returns $true, Get-Content will be called, and the Path parameter value will be passed to it. If not, Add-Content will first be called with the Path parameter passed to it, and then Get-Content will be called right afterward. Building a good unit test for this requires checking both of these scenarios.

I'll build my describe block and have my two It blocks with both scenarios inside:

describe 'Get-FileContents' {
     it 'creates a file then reads if the file does not exist' {
     }
     it 'only reads the file if it already exists' {
     }
 }

To get started building mocks, you must foresee all of the commands that will be called for each scenario. For both scenarios, Test-Path is going to be called by testing to see if the file exists. For the first scenario (file exists), it will then call Get-Content using the value of the Path parameter. For the second scenario, Add-Content and Get-Content will be called using the value of the Path parameter.

Knowing this, I can now begin building my mocks and putting the function call inside of the It blocks.

describe 'Get-FileContents' {
     it 'creates a file then reads if the file does not exist' {
         mock -CommandName 'Test-Path' –MockWith {
         }
         mock -CommandName 'Get-Content' –MockWith {
         } 
         mock -CommandName 'Add-Content' –MockWith {
         }
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
     it 'only reads the file if it already exists' {
         mock -CommandName 'Test-Path' –MockWith {
         }
         mock -CommandName 'Get-Content' –MockWith {
         } 
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
 }

A mock has a name, which is the same name as the command you'd like to mock, and a MockWith parameter that indicates what to return instead of running the actual command itself. By applying these mocks and then running the function, any command that is mocked will not run. Instead, it will just return what I have in the MockWith ScriptBlock.

For this example, I'm not concerned with what Get-Content and Add-Content return. I just want to ensure that they run according to the scenario. In this case, I can just have them all return $null. We'll save Test-Path until later.

describe 'Get-FileContents' {
     it 'creates a file then reads if the file does not exist' {
         mock -CommandName 'Test-Path' –MockWith {
             
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         } 
         mock -CommandName 'Add-Content' –MockWith {
             return $null
         }
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
     it 'only reads the file if it already exists' {
         mock -CommandName 'Test-Path' –MockWith {
             
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         } 
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
 }

I want to be sure that each of my mocks was called in each scenario without depending on a file being available or not. To do that, I have to "trick" Test-Path into returning a $true or $false at my choosing. Instead of returning a $null value, I'll have Test-Path return either $true or $false, depending on the scenario I'm testing.

describe 'Get-FileContents' {
     it 'creates a file then reads if the file does not exist' {
         mock -CommandName 'Test-Path' –MockWith {
             return $false  
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         } 
         mock -CommandName 'Add-Content' –MockWith {
             return $null
         }
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
     it 'only reads the file if it already exists' {
         mock -CommandName 'Test-Path' –MockWith {
             return $true            
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         } 
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
     }
 }

I'm now sure that Test-Path will return $false when "the file does not exist" and $true when it "does exist." This allows me to test each branch of the function separately.

Now I need to add my assertions, which means I need to test whether my mocks are being called or not, depending on the scenario. To do this, I'll use the Assert-MockCalled command. This is a Pester command that allows me to check to see if my mocks were indeed called.

describe 'Get-FileContents' {
     it 'creates a file then reads if the file does not exist' {
         mock -CommandName 'Test-Path' –MockWith {
             return $false  
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         }
         mock -CommandName 'Add-Content' –MockWith {
             return $null
         }
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
         Assert-MockCalled –CommandName 'Get-Content' –Times 1 –Scope It
         Assert-MockCalled –CommandName 'Add-Content' –Times 1 –Scope It
     }
     it 'only reads the file if it already exists' {
         mock -CommandName 'Test-Path' –MockWith {
             return $true            
         }
         mock -CommandName 'Get-Content' –MockWith {
             return $null
         } 
         Get-FileContents –Path 'C:\SomeBogusFile.txt'
         Assert-MockCalled –CommandName 'Get-Content' –Times 1 –Scope It
     }
 }

Notice that I specified the mock I'd like to check, and (optionally) I chose to test that each mock was only called once. Also optionally, I decided to apply a Scope to each assertion. This is a best practice because it limits confusion if you have mocks called in the parent describe block as well. It is best practice to be as verbose as possible when creating assertions.

I'll now call my test, and you can see that all tests passed.

Code testing

Code testing

Conversely, if I remove the Get-Content reference from the code, for example, this would happen:

Get-Content reference removed from the code

Get-Content reference removed from the code

Mocking is a great way to remove command dependencies in your code. If you're writing unit tests for code with command references, always remember to mock them out even if you don't intend to add assertions. This will ensure that only the code inside your function is being executed.

In my next post, I will introduce you to PowerShell integration testing, which allows you to verify the behavior of your entire script and not just single units.

Subscribe to 4sysops newsletter!

For a full breakdown on what's possible with the PowerShell testing framework Pester check out The Pester Book by myself and Don Jones.

1 Comment
  1. Ronald Koornneef 2 years ago

    This is a very good explanation on mocking, thanks!

    I do have an issue with a similar (simple) function that needs to be mocked. The hypothetical situation is that i require to write a unit test that test the following function:

    Function Open-Taskmanager ()
    {
        Start-Process Taskmgr
    }
    Open-Taskmanager

    When using a similar mock it does not seem to work:

    Describe "Mock opening TaskManager" {
        It 'Mocks opening taskmanager' {
            Mock -CommandName 'Start-Process' -MockWith {
                return $null
            }
            Mock -CommandName 'Open-Taskmanager' -MockWith {
                return $null
            }
            Assert-MockCalled –CommandName 'Start-Process' –Times 1 –Scope It
        }
    }

    It always keeps running the script and opening taskmanager instead of mocking it.

    I am sure I'm doing something wrong but can't figure out what.

Leave a reply to Ronald Koornneef Click here to cancel the reply

Please enclose code in pre tags

Your email address will not be published.

*

© 4sysops 2006 - 2022

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account