DEV Community

Olivier Miossec
Olivier Miossec

Posted on • Edited on

Unit testing in PowerShell, Pester and Mocking

In the introduction to Unit Testing in PowerShell with Pester we see that the goal of Pester is to test what we have written and see if any change we made is safe and doesn’t break anything.

In PowerShell, like in many other languages, you will use external libraries, modules or functions. How can you deal during the test phase? How to be sure that the error you get come from your code or is caused by an external thing?

The response is to use Pester. Pester can be used to perform several test types, unit tests, integration tests, and some others. But in testing, there are two rules.

  • Tests need to cover your creation and not external components
  • Tests should always return the same result for the same inputs.

When writing PowerShell scripts, modules or resources, you often rely on external sources. It could be a function, a web service, or any other object. When writing a pester script against that the purpose is to remove them from the test process.

If you have a function comparing data from active directory and a web service. There is some calculation, a regex or data extractions from json. How to make sure the test will not rely on active directory cmdlet and the web service. They can be unavailable during a test (and they must be). You need to make sure that for a defined set of data you have the same result (the test is predictable).

More if you need to test a function in your code, you want to test only this function and not other functions in your code that may be called during the execution.

To write reliable test scripts, external calls must be disabled and replaced by something that can be fake and that can be predictable.

This is the purpose of mocking. It mocks or fakes the behavior and the result of a command. More, the original command is not executed.

Mocking a command is simple. You need to use the Mock keyword in a Describe or Context scope.

Mock get-something { } 
Enter fullscreen mode Exit fullscreen mode

Tests in the same scope will use the mocked command instead of the real command.

Scoping is important to understand. If a mock is created inside a Context it will be available only in this context, if the mock is written in the describe scope, it can be available to all scopes including all contexts.

Let’s start with a simple example. Two functions in a script. One of these functions calls the other to make some calculations.

function get-stuff {
    return Get-Random -Maximum 10 -Minimum 1
}

function get-otherStuff {
    $stuff = get-stuff 
    return $stuff + 1
}
Enter fullscreen mode Exit fullscreen mode

How to make sure that the function get-OtherStuff works correctly in any situation. If we want to test this function and only this function it needs to be isolated from any external component.

Describe "How to mock a function" {
    Mock get-stuff { } 
    it "should return 1" {
        get-otherStuff | Should -be 1
    }
}
Enter fullscreen mode Exit fullscreen mode

But doing so we can only test one situation; Get-Stuff returns nothing, to test another situation we need to use a different scope.

Describe "How to mock a function" {
    context 'Where get-stuff return nothing' {
        Mock get-stuff { }  
        it "should return 1" {
            get-otherStuff | Should -be 1
        }
    }
    context 'Where get-stuff return 5' {
        Mock get-stuff -MockWith { 5 } 
        it "should return 6" {
            get-otherStuff | Should -be 6
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Context is used here as a separator; it defines two spaces where the two mocks can return a different result.

But often testing means to check the behavior of a function when different parameter values are used.

Take these two functions. Testing get-otherStuff can be difficult as it uses a random number to call get-stuff.

function get-stuff {
    param (
        [int]
        $limit
    )
    if ($limit -gt 99) {
        return 100
    } else {
        return Get-Random -Maximum 10 -Minimum 1
    }   
}
function get-otherStuff {
    param (
        [int]
        $limit = 10
    )
    $stuff = get-stuff -limit $limit
    return $stuff + 1
}

Describe "How to mock a function" {
        Mock get-stuff -MockWith  { 10 }  -ParameterFilter { $limit -eq 30 }
        Mock get-stuff -MockWith  { 100 }  -ParameterFilter { $limit -eq 100 }
        it "should return 11" {
            get-otherStuff -limit 30 | Should -be 11
        }
        it "should return 101" {
            get-otherStuff -limit 100 | Should -be 101
        }
}
Enter fullscreen mode Exit fullscreen mode

ParameterFilter act as a condition, just like in an If statement. It can combine several tests. The bloc should return a Boolean value.

The first example used only scalar values. Most of the time function and cmdlet return objects and not a single value. Get-item returns a FileInfo object, most of my function return pscustomObject or a typed object.

This object must be faked during the test.

function get-fileExtension
{
    param (
        [string]
        $filepath
    )
    try {
        $FileInfo = get-item -Path $filepath 
        return $FileInfo.Extension
    } catch {
        Write-Error -Message " Exception Type: $($_.Exception.GetType().FullName) $($_.Exception.Message)"
    }
}

Describe "How to mock a function" {
    Mock get-item -MockWith {
        [pscustomobject]@{
            "Extension"         = "txt"
            "Length"            = 8655
            "LastAccessTime"    = "lundi 29 avril 2019 07:50:52"
        }
     } 
     it "return txt" {
        get-fileExtension -filepath test.txt | Should -be "txt"
     }
}
Enter fullscreen mode Exit fullscreen mode

PsCustomObject can fake data in many situations, but what if we need to return a specific type of object. It could be your class or standard object type.

New-MockObject can be used to fake almost any type.

function Test-LocalFile
{
    param (
        [string]
        $filepath
    )
    try {
        $FileInfo = get-item -Path $filepath -ErrorAction SilentlyContinue
        if ($FileInfo.getType().Name -eq "FileInfo") {
            return $true
        }
    } catch {
        Write-Error -Message " Exception Type: $($_.Exception.GetType().FullName) $($_.Exception.Message)"
    }
}

Describe "How to mock a function" {
    Mock get-item  {
        New-MockObject -Type "System.IO.FileInfo"
    }
     it "return txt" {
        test-LocalFile -filepath test.txt | Should -BeTrue
     }
}
Enter fullscreen mode Exit fullscreen mode

Now that we see how we can mock a command in Pester how can we be sure that the mocked commands are called?

Sometimes we need to know if an external command has been called or not, or how many times during the execution of code.

Pester offers two commands to verify that

Assert-MockCalled to verify if a command has been called

Assert-MockCalledGet-Item 
Enter fullscreen mode Exit fullscreen mode

If not an exception is raised.
Adding an integer let you test if the command has to be called x or less time

Adding an integer with -Exactly will test if the command has call x time the

Assert-MockCalledGet-Item-Exactly1 
Enter fullscreen mode Exit fullscreen mode

Using 0 will test if the command is not called

Assert-MockCalledGet-Item0 
Enter fullscreen mode Exit fullscreen mode

Sometimes functions and cmdlet are not available during the test.

For example, if a function uses Get-AzContext from the Azure PowerShell module to extract some information. You don’t want to install the AZ module or login to Azure to perform the test.

You cannot mock the cmdlet, as it doesn’t exist in the session that will run the test. But you also need the result.

We can use the scoping in pester to deal with this. Remember everything that exists in a scope exists only in this scope. So, we can overwrite the function

function test-MyAzContext {
    $AzContextObject = get-AzContext 
    if ($AzContextObject.Account -eq "Olivier.miossec@test.me") {
        return  $true
    }
    else {
        return $false
    }
}

Describe "How to mock a function" {
    function get-AzContext () {
        [pscustomobject]@{
            "Name"         = "Test SUb"
            "Account"      = "Olivier.miossec@test.me"
        }
     } 
     it "return true" {
        test-MyAzContext | Should -BeTrue
     }
}
Enter fullscreen mode Exit fullscreen mode

Testing with Pester can be complicated, but what I learned with mocking is that you need to know exactly how your function work and what they need as input. Using Mock with Pester improves coding still and you will get a better understanding of the interaction with external commands and how your code should work.

Top comments (0)