1

I am an utter newbie regarding PowerShell and am now tasked to write unit tests for some existing PowerShell scripts. It is a great task for me to learn about automating unit tests but have no idea where to begin. I followed several trainings about Pester and how to create unit tests with it and so far so good. Now comes the point to actually write a good test while using mocks and it is killing me. Even for a simple function that checks if a certain process is running and if it is killing it.

The function is as follows:

function Close-RunningApplications()
{
    # Stop Task Manager, servcies.msc, Event Viewer, sysinternal ProcessExplorer etc. - sometimes keeps services from being delete correctly
    $Process = Get-Process Taskmgr -ErrorAction SilentlyContinue
    if($Process)
    {
        Write-Host "==| PreReq:: Close Task Manager"            -foregroundcolor DarkCyan
        taskkill /F /IM Taskmgr.exe
    }

    $Process = Get-Process mmc -ErrorAction SilentlyContinue
    if($Process)
    {
        Write-Host "==| PreReq:: Microsoft Management Console"  -foregroundcolor DarkCyan
        taskkill /F /IM mmc.exe
    }

    $Process = Get-Process procexp64 -ErrorAction SilentlyContinue
    if($Process)
    {
        Write-Host "==| PreReq:: Sysinternals Process Explorer" -foregroundcolor DarkCyan
        taskkill /F /IM procexp64.exe
    }
}

Close-RunningApplications | Write-Verbose -Verbose

Now I have written some tests for this to see some mocking functionality and I was surprised that all tests passed. It was only later that I realized that my tests actually run the tested script as well (running the tests with TaskManager open will actually close it)

# Get current working directory and set up script under test
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'

# Include the script to test to make the defined functions available to the tests
. "$here\$sut"

$CommandName = $sut.replace(".ps1",'')

Describe "Tests for the $CommandName Function" {
    It "Command $CommandName exists" {
        Get-Command $CommandName -ErrorAction SilentlyContinue | Should Not Be NullOrEmpty
    }

    BeforeAll { Mock -CommandName 'Close-RunningApplications' {return 1}
                Mock taskkill {return 2}
                Mock Get-Process {$Process}
    }    

    Context "Mocks Taskmanager" {

        $Process = "Taskmgr"
        $result = Get-Process

        It "Mocks opening and closing Taskmanager" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
        } 
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }

    Context "Mocks Microsoft Management Console" {

        $Process = "mmc"
        $result = Get-Process

        It "Mocks opening and closing Microsoft Management Console" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
        }
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }

    Context "Mocks Sysinternals Process Explorer" {

        $Process = "procexp64"
        $result = Get-Process

        It "Mocks opening and closing Sysinternals Process Explorer" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
        }
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }
    It "Asserts the totall mocks Get-Process" {
         Assert-MockCalled 'Get-Process' -Exactly 3
    }
    It "Asserts the totall mocks Close-RunningApplications"{
        Assert-MockCalled 'Close-RunningApplications' -Exactly 3
    }
    It "Asserts the totall mocks taskkill"{
        Assert-MockCalled 'taskkill' -Exactly 3
    }
}

I am a bit at a loss as to mocking..

2 Answers2

1

There are a couple of issues with your tests...

  • You can't mock invocation of external programs (e.g. taskkill.exe) in Pester. You might notice something like Type "TASKKILL /?" for usage. in the output from pester because your taskkill | Should Be 2 is still calling the external taskkill.exe program rather than your mock.

  • By mocking Close-RunningApplications with Mock 'Close-RunningApplications' {1} you're not actually testing any of your code, which makes your entire test suite a bit pointless. The idea with Pester is to invoke your own functions as-is, but mock all of the function calls it makes that touch external integration points (databases, filesystem, process list, etc).

Here's how I'd test your Close-RunningApplications...

To address the first issue, write a wrapper cmdlet around taskkill.exe so you can mock calls to the Invoke-TaskKill function:

function Invoke-TaskKill
{
    param( [string] $ImageName )
    taskkill /F /IM $ImageName
}

and then call this from Close-RunningApplications instead:

function Close-RunningApplications()
{

    $Process = Get-Process Taskmgr -ErrorAction SilentlyContinue
    if($Process)
    {
        Write-Host "==| PreReq:: Close Task Manager"            -foregroundcolor DarkCyan
        Invoke-TaskKill Taskmgr.exe
    }

    ... etc ...

}

Then, fix the second issue by dropping the Mock 'Close-RunningApplication' and use Mock Invoke-TaskKill instead. Your test Contexts might then looksomething like this, depending on what yo're actually trying to test:

    Context "Mocks closing Taskmanager when running" {
        Mock Get-Process -ParameterFilter { $Name -eq "Taskmgr" } `
                         -MockWith { return $Name }
        Mock Get-Process { }
        Mock Invoke-TaskKill { }
        It "Mocks closing Taskmanager" {
            Close-RunningApplications
            Assert-MockCalled 'Get-Process' -Exactly 3
            Assert-MockCalled 'Invoke-TaskKill' -ParameterFilter { $ImageName -eq "Taskmgr.exe" } -Exactly 1
            Assert-MockCalled 'Invoke-TaskKill' -ParameterFilter { $ImageName -ne "Taskmgr.exe" } -Exactly 0
        }
    }

    Context "Mocks skipping Taskmanager when not running" {
        Mock Get-Process { }
        Mock Invoke-TaskKill { }
        It "Mocks skipping Taskmanager" {
            Close-RunningApplications
            Assert-MockCalled 'Get-Process' -Exactly 3
            Assert-MockCalled 'Invoke-TaskKill' -Exactly 0
        }
    }

The first context makes sure Close-RunningApplications calls Invoke-TaskKill -ImageName Taskmgr.exe when Task Manager is running, and the second ensures it skips the call when Task Manager is not running.

You can duplicate this for your other external processes to match.

Note - I collapsed your multiple It / Asserts into a single It / Assert / Assert. Some people might protest that you shouldn't assert more than one thing in a test, but pragmatically I think the Asserts in this case are all really testing the same piece of behaviour so it's fine to combine them, but feel free to modify to suit your coding style.

Note also - I removed the BeforeAll and added duplicate Mocks for Get-Process and Invoke-TaskKill in each Context. I prefer more strongly isolated tests even if it means a bit of duplication, but again, adapt to your tastes...

mclayton
  • 8,025
  • 2
  • 21
  • 26
  • Thanks a lot! That really points me in the right directions. We want to add some more checks in the functions but that adds to the existing invocation of TaskKill so I think I can add this to the test. I do have 1 question though. When running the test for simplified function you mentioned (on all 3 processes) and I do a coverage check I get 93.33%. It seems to miss the taskkill /F /IM $ImageName command. I would have thought that by Mocking Invoke-TaskKill it would have covered that. – RonaldKoornneef81 Jan 02 '20 at 07:05
  • If you're mocking a function (e.g. ```Invoke-TaskKill```) then the actual code inside the real implementation will never be executed, so won't be included in the code coverage percentage.. – mclayton Jan 02 '20 at 22:12
0

I have done some research myself and have found the answer.

The problem is the actuall call to the function which is outside the scriptblock. Pester cannot really handle that so always the call will be made. There is an additional module in PowerShell (ImportTestScript) which allows you to shadow the actual call with a "fake" one

Install-Module -Name ImportTestScript

I have altered my initial testscript as followed to get around it:

#Import script module so the function call can be shadowed the 
Import-Script `
    -EntryPoint Close-RunningApplications `
    -Path $PSScriptRoot/CloseRunningApplications.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path

Describe "Standard tests for the  Function." {      

    BeforeAll { Mock 'Close-RunningApplications' {1}
                Mock 'taskkill' {return 2}
                Mock 'Get-Process' {$Process}
                Mock 'Write-Host' {return "test"}
    }

    Context "For test purposes" {
    #Checks whether the correct file is being tested.
        It "Command Close-RunningApplications exists" {
            Get-Command Close-RunningApplications -ErrorAction SilentlyContinue | Should Not Be NullOrEmpty
        }

    #Checks whether the file tested is valid PowerShell code
        It "Close-RunningApplications is valid PowerShell code" {
            $psFile = Get-Content -Path "$here\CloseRunningApplications.ps1" `
                              -ErrorAction stop
            $errors = $null
            $null = [System.Management.Automation.PSParser]::Tokenize($psFile, [ref]$errors)
            $errors.Count | Should Be 0
        }
    }

    Context "Mocks Taskmanager" {

        $Process = "Taskmgr"
        $result = Get-Process

        It "Mocks opening and closing Taskmanager" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
            Write-Host | Should Be "test"
        } 
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }

    Context "Mocks Microsoft Management Console" {

        $Process = "mmc"
        $result = Get-Process

        It "Mocks opening and closing Microsoft Management Console" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
        }
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }

    Context "Mocks Sysinternals Process Explorer" {

        $Process = "procexp64"
        $result = Get-Process

        It "Mocks opening and closing Sysinternals Process Explorer" {
            $result | Should Be "$Process"
            taskkill | Should Be 2
            Close-RunningApplications | Should Be 1
        }
        It "Asserts called mocks Get-Process"{
            Assert-MockCalled 'Get-Process' -Exactly 1
        }
        It "Asserts called mocks Close-RunningApplications"{
            Assert-MockCalled 'Close-RunningApplications' -Exactly 1
        }
        It "Asserts called mocks taskkill"{
            Assert-MockCalled 'taskkill' -Exactly 1
        }
    }
}