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

2

6

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.

Stoeber answered 20/12, 2017 at 20:34 Comment(0)
E
4

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.

Easterling answered 20/12, 2017 at 20:54 Comment(2)
I like the -NoParallel idea! if I can't figure out something else, that may be the way to go. Currently investigating PowerShell workflows at a colleague's recommendation.Stoeber
@Stoeber workflows have all kinds of restrictions, and will be even more difficult to test with something like Pester. Additionally, workflows are not going to be supported going forward in PowerShell Core (Windows PowerShell is not expected to have future updates), so I would recommend against workflows for new projects.Easterling
P
0

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'
Pilsudski answered 6/1, 2022 at 22:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.