jrizzo
jrizzo

Reputation: 1692

How to use PowerShell multithreading and still unit test with Pester Mocks

I'm trying to do a simple parallel operation in Powershell. I am using PoshRSJobs for multithreading, though I have also tried Invoke-Parallel with the same issue. I need to call a couple of my own functions in the scriptbody of the job, but this does not allow me to MOCK those functions for unit testing (they end up being the original non-mocked functions). At this point, I'm just trying to assert that they have been called the correct number of times.

Here is the original class (the functionality of the imported modules are irrelevant - the actual implementations are currently returning test strings)...

Import-Module $PSScriptRoot\Convert-DataTable
Import-Module $PSScriptRoot\Get-History
Import-Module $PSScriptRoot\Get-Assets
Import-Module $PSScriptRoot\Write-DataTable

function MyStuff (
    param(
        [string]$serverInstance = "localhost\SQLEXPRESS", 
        [string]$database = "PTLPowerShell",
        [string]$tableName = "Test"
    )
    $assets = Get-Assets
    $full_dt = New-Object System.Data.DataTable
    $assets | Start-RSJob -ModulesToImport $PSScriptRoot\Convert-FLToDataTable, $PSScriptRoot\Get-FLHistory {
        $history = Get-History $asset
        $history_dt = Convert-DataTable $history
        return $history_dt.Rows
    } | Wait-RSJob | Receive-RSJob | ForEach { 
        $full_dt.Rows.Add($_) 
    }
    Write-DataTable $serverInstance $database $tableName $full_dt
}

Here is the Pester test...

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"

Describe "MyStuff" {
    BeforeEach {
        Mock Get-Assets { return "page1", "page2"}
        Mock Get-History { return "history" }
        Mock Convert-DataTable { 
            $historyDT = New-Object System.Data.Datatable;
            $historyDT.TableName = 'Test'
            return ,$historyDT
        }
        Mock Write-DataTable {}
    }
    It "should do something" {
        { MyStuff } | Should -Not -Throw;
    }
    It "should call Get-FLAssetGrid" {
        Assert-MockCalled Get-Assets 1
    }
    It "should call Get-FLHistory" {
        Assert-MockCalled Get-History 2
    }
    It "should call Convert-DataTable" {
        Assert-MockCalled Convert-DataTable 2
    }
    It "should call Write-DataTable" {
        Assert-MockCalled Write-DataTable 1
    }
}

Here is the Pester test's output currently...

Describing MyStuff
  [+] should do something 1.71s
  [+] should call Get-Assets 211ms
  [-] should call Get-History 61ms
    Expected Get-History to be called at least 2 times but was called 0 times
    23:         Assert-MockCalled Get-History 2
    at <ScriptBlock>, myFile.Tests.ps1: line 23
  [-] should call Convert-DataTable 110ms
    Expected Convert-DataTable to be called at least 2 times but was called 0 times
    26:         Assert-MockCalled Convert-DataTable 2
    at <ScriptBlock>, myFile.Tests.ps1: line 26
  [+] should call Write-DataTable 91ms

So ultimately, I'm looking for a way to do parallel operations in PowerShell and still be able to mock and unit test them.

Upvotes: 6

Views: 752

Answers (2)

Gregor y
Gregor y

Reputation: 2050

I was able to sorta get it to work via injecting the mock into the thread; here's a prof of concept but the fine details would need to be hammered out on a case by case basis

#code.ps1
function ToTest{
   start-job -Name OG -ScriptBlock {return (Get-Date '1/1/2000').ToString()}
}

pester

#code.Tests.ps1
$DebugPreference = 'Continue'
write-debug 'Pester''ng: code.ps1'
#################################################################
. (join-path $PSScriptRoot 'code.ps1')
Describe 'Unit Tests' -Tag 'Unit' {
   Mock start-job {
      $NewSB = {
         &{describe 'MockingJob:$JobName' {
            Mock get-date {'got mocked'}

            & {$ScriptBlock} | Export-Clixml '$JobName.xml'
         }}
         $out = Import-Clixml '$JobName.xml'
         remove-item '$JobName.xml'
         $out | write-output
      }.ToString().Replace('$ScriptBlock',$ScriptBlock.ToString()).Replace('$JobName',$Name)
      start-job -Name "Mock_$Name" -ScriptBlock ([ScriptBlock]::Create($NewSB))
   } -ParameterFilter {$Name -NotMatch 'Mock'}

   It 'uses the mocked commandlet' {
      $job = ToTest
      receive-job -Job $job -wait | should be 'got mocked'
      remove-job  -Job $job
   }
}
$DebugPreference = 'SilentlyContinue'

Upvotes: 0

briantist
briantist

Reputation: 47802

I don't consider this a full answer, and I don't work on the Pester project, but I would say that this is simply not a supported scenario for Pester. This might change when/if concurrent programming becomes part of PowerShell proper (or it may not).

If you're willing to change your implementation you might be able to write around this limitation to support some sort of testing.

For example, maybe your function doesn't use an RSJob when it only has 1 thing to do (which conveniently might be the case when testing).

Or maybe you implement a -Serial or -NoParallel or -SingleRunspace switch (or a -ConcurrencyFactor which you set to 1 in tests), wherein you don't use a runspace for those conditions.

Based on your example it's difficult to tell if that kind of test adequately tests what you want, but it seems like it does.

Upvotes: 4

Related Questions