1

I am working on writing Pester tests for our PowerShell scripts that are used during task sequences. Several of them work with the task sequence variables and so I wrote a mock that allows for testing reading variables and am now trying to figure out how to do it for writing variables. This is the code to read a task sequence variable:

$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$Value = $TsEnv.Value('VariableNameToRead')

By passing in the $TsEnv to a function I can then mock it with the following:

$TsEnv = @{
    'VariableNameToRead' = 'TestValue'
}
Add-Member -InputObject $TsEnv -MemberType ScriptMethod -Name Value -Value {
    Param( [String]$Key )
    $This[$Key]
} 

This is the code for writing a task sequence variable:

$TsEnv = New-Object -COMObject Microsoft.SMS.TSEnvironment
$TsEnv.Value('VariableNameToWrite') = 'ValueToWrite'

With it being in parentheses after the $TsEnv.Value I am thinking it is treating it as a method, but I am unable to find any examples on how to assign values to a method.

  • I'd probably just write a wrapper function and mock that instead. – derekbaker783 Apr 29 '20 at 18:41
  • Where are you using Pester, and which version are you using? – derekbaker783 Apr 29 '20 at 18:47
  • 1
    I've been thinking about doing that. I'm not super gung-ho about having every little thing tested, but I figured I would ask here first in case. I get a nice case of the warm fuzzies when I am able to successfully write unit tests for all my functions. – colinlwebster Apr 29 '20 at 18:48
  • Right now it is just being ran on my machine outside of a task sequence before being committed to a code repository, but long term we are looking at having a jenkins job run it for us. Pester version in our environment right now is 3.4.0 – colinlwebster Apr 29 '20 at 18:51

2 Answers2

0

With Pester 4.3.3+, you might be able to use New-MockObject to create a usable mock of that COM object.

Alternatively, you can do something similar to the below to allow you to mock the functionality of the COM object.

If that COM object is available on the machines where your CI is running, I might consider skipping the mocks and writing an integration test.

# functions.ps1

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";


function GetTaskSequenceValue(
    [Parameter(Mandatory=$true)]
    [string] $varNameToRead,    

    [Parameter(Mandatory=$false)]
    [System.Management.Automation.ScriptBlock] $readAction = {
        param([string] $readKey)
            $tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
            $tsEnv.Value($readKey)
    }
) {
    $value = Invoke-Command `
        -ScriptBlock $readAction `
        -ArgumentList @($varNameToRead)

    return $value
}


function SetTaskSequenceValue(
    [Parameter(Mandatory=$true)]
    [string] $varNameToWrite,   

    [Parameter(Mandatory=$false)]
    [System.Management.Automation.ScriptBlock] $writeAction = {
        param([string] $writeKey, [string] $value)
            $tsEnv = New-Object -COMObject 'Microsoft.SMS.TSEnvironment'
            $TsEnv.Value($writeKey) = $value   
    }    
) {    
    try {
        Invoke-Command `
            -ScriptBlock $writeAction `
            -ArgumentList @($varNameToWrite)

        return $true
    }
    catch {
        # Swallow it
    }
    return $false
}

Tests for the functions abov. The tests manually mock out the COM objects

# functions.test.ps1

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


Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop";


Describe "GetTaskSequenceValue" {
    It "gets the expected value" {
        $expected = 'value'
        $mockAction = { 
            param($dummy)  
                return 'value'
        }
        $actual = GetTaskSequenceValue `
            -varNameToRead 'dummyName' `
            -readAction $mockAction
        $actual | Should Be $expected
    }
}


Describe "SetTaskSequenceValue" {
    It "sets the expected value" {
        $expected = 'value'
        $mockAction = { 
            param($dummy)  
                return 'value'
        }
        $actual = SetTaskSequenceValue `
            -varNameToWrite 'dummyValue' `
            -writeAction $mockAction 
        $actual | Should Be $true
    }
}

derekbaker783
  • 8,109
  • 4
  • 36
  • 50
  • I did not think about using Invoke-Command, but the only issue I have with this is that the line of code in the SetTaskSequenceValue function `$TsEnv.Value($writeKey) = $value` does not actually get tested. I think of it much like being the following pseudo-snippet: `If ($ActualExecution) { $TsEnv.Value($writeKey) = $value } Else { return 'value' }` In this case the test is only ever testing the Else path so the If path never actually gets tested. – colinlwebster May 08 '20 at 19:25
  • @colinlwebster, right. You should probably just write an integration test, or see if you can mock the actual .NET objects after upgrading to a newer version of Pester. – derekbaker783 May 08 '20 at 19:30
0

Anything to deal with getting environment variables, WMI, or dotnet static method calls, I like to contain within a small helper function, then it's very easy to mock it. Here's what that helper could look like.

Function Get-SMSTsVariable{($VariableName)
    return $TSEnv.Value($VariableName)
}

Then you can easily mock this in various contexts to check and see how your code acts when various environmental variables are set.

For instance, if you want it to return a value of BitLockerProvisioning when you run Get-SMSTsVariable -VariableName _SMSTSCurrentActionName, and to return 'C:' when you run _OSDDetectedWinDir you setup a mock like this:

mock Get-SMSTsVariable `
   -parameterFilter { $VariableName -eq '_SMSTSCurrentActionName'} `
   -mockWith {return 'BitLockerProvisioning'}

mock Get-SMSTsVariable `
   -parameterFilter { $VariableName -eq '_OSDDetectedWinDir'} `
   -mockWith {return 'C:'}

In this way, you can begin your test setting up a handful of responses for the various ways your functions operate. It's really a breeze.

FoxDeploy
  • 12,569
  • 2
  • 33
  • 48
  • This is pretty much what I am doing for testing getting environment variables. The issue I am having is with testing setting environment variables. – colinlwebster May 08 '20 at 19:28