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.

3 Answers 3

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.

Sign up to request clarification or add additional context in comments.

2 Comments

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.
@jrizzo 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.
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'

Comments

0

I made a gist here: https://gist.github.com/powercode/dabd290a969fc524a73140ab3f3414a8#file-noparallel-foreach-object-ps1

It allows you to use Foreach-Object -Parallel in your scripts, but by loading the script in the gist, it replaces Foreach-Object with a version that runs the parallel scriptblock in the same runspace as the caller (i.e. no parallelism during the test). When doing this, Pester mocking works as expected.

Describe MyScriptTest {
    BeforeAll {
        . $PSScriptRoot\NoParallel.Foreach-Object.ps1
    }
    It FakeRunsInParallel {
        Mock Set-Content { } -Verifiable
        # to demonstrate that $using: works in parallel
        $Offset = 10
        1..10 | ForEach-Object -Parallel {
            $offsetValue = $_ + $using:Offset
            Set-Content -Path:"$_.txt" -Value:$offsetValue
        }

        Should -Invoke Set-Content -Times:10 -Exactly -ParameterFilter { [int]$Value[0] -gt 10 }
    }
}

Describe MyModuleTest {
    BeforeAll {
        # create a module with a function that uses ForEach-Object -Parallel
        $moduleUnderTest = New-Module -Name MyModule -Scriptblock {
            function Set-ParallelContent {
                # to demonstrate that $using: works in parallel
                $Offset = 10
                1..10 | ForEach-Object -Parallel {
                    $offsetValue = $_ + $using:Offset
                    Set-Content -Path:"$_.txt" -Value:$offsetValue
                }

            }
        } | Import-Module -PassThru

        # replace Foreach-Object with the NoParallel version in the module under test
        . $moduleUnderTest $PSScriptRoot\NoParallel.Foreach-Object.ps1
    }
    It FakeRunsInParallel {
        Mock -ModuleName:MyModule Set-Content { } -Verifiable

        # call function in module that uses ForEach-Object -Parallel
        Set-ParallelContent

        Should -Invoke -ModuleName:MyModule -CommandName:Set-Content -Times:10 -Exactly -ParameterFilter: { [int]$Value[0] -gt 10 }
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.