Mocking Missing Cmdlet ErrorAction with Pester

Following on from the previous Mocking Missing Cmdlets with Pester post, I also encountered another interesting problem when attempting to mock cmdlets that were not present on the test system. This one is more of an edge-case, hence its own post. Just like last time, the tests worked when I had the Hyper-V cmdlets installed, but failed when running within an Appveyor VM.

It’s probably not uncommon that you will need to ensure that the code under test should throw an error here-and-there. Here is a pseudo-example that tests that Get-VM writes an error when passed with a non-existent VM name:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

When this test is run it fails:

Describing Mocking ErrorAction Example
 Write-Error 'Oops'  : Oops
At C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1:709 char:21
+                     & $___ScriptBlock___ @___BoundParameters___ @___ArgumentList ...
+                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Write-Error], WriteErrorException
    + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException 
 [-] Get-VM Throws 176ms
   Expected: the expression to throw an exception
   at line: 6 in D:\Users\Iain\Desktop\PesterDemo.Tests.ps1
Tests completed in 176ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

The problem here is that the stub function is not an advanced function and the –ErrorAction preference switch is ignored! This is easily resolved by adding the [CmdletBinding()] attribute to the stub function definition:

Describe 'Mocking ErrorAction Example' {
    Function Get-VM { [CmdletBinding()] param ($Name) }
    InModuleScope 'xVMHyper-V' {

        It 'Get-VM Throws' {
            Mock Get-VM -ParameterFilter { $Name -eq 'TestVM' } -MockWith { Write-Error 'Oops' }
            { Get-VM -Name 'TestVM' -ErrorAction Stop } | Should Throw;
        }
    }
}

Running the test now results in the expected output:

Describing Mocking ErrorAction Example
 [+] Get-VM Throws 177ms
Tests completed in 177ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

This is not a Pester issue and it’s not a Powershell issue either. It’s just the way the normal Powershell functions work. But, just in case someone else runs into it I thought it would be worth quickly documenting!

Mocking Missing Cmdlets with Pester

When writing Pester unit tests for your Powershell code you will probably have a need to mock calls to external functions before too long. This process works as you would expect when Pester can locate a defined function/cmdlet with a matching name. However, if Pester cannot find a definition, it will fail.

This problem will normally surface in a Continuous Integration (CI) environment. For me, it was writing the first suite of tests for the xHyper-V DSC resource module. The Hyper-V cmdlets where present on my authoring machine but were not present on the Appveyor build VM.

Here is an overly simplified Pester test that I’ll use for demonstration purposes:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

If we run the test and Pester cannot locate a defined function then it will report an error. Note: if you run this on a machine with the Hyper-V module installed (and you have a VM called ‘TestVM’) then it will pass – but you knew that already ;).

Describing Missing Cmdlet Mocking Example
 [-] calls Get-VM 474ms
   Could not find Command Get-VM
   at line: 600 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 474ms

This is easily overcome by defining an empty function within the test file. Note: this will need to be defined within the ‘InModuleScope’ script block if you’re testing a module’s internals.

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM -Scope It;
        }

    }
}

The test now passes as we would expect. Yay \o/

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 141ms
Tests completed in 141ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Now, what you really need to know is that for Pester to enumerate and mock parameter filters, those parameters need to be defined on the stub function. If we were to update the test to check for the passing of a particular –Name parameter like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

When we run the test it will now fail again.

Describing Missing Cmdlet Mocking Example
 [-] Calls Get-VM 131ms
   Expected Get-VM to be called at least 1 times but was called 0 times
   at line: 518 in C:\Program Files\WindowsPowerShell\Modules\Pester\Functions\Mock.ps1
Tests completed in 131ms
Passed: 0 Failed: 1 Skipped: 0 Pending: 0

For Pester to enumerate the dynamic parameters on the function, it needs to have the parameters (only the one’s you’re interested in) defined. This can easily be fixed like so:

Describe 'Missing Cmdlet Mocking Example' {
    InModuleScope 'xVMHyper-V' {

        Function Get-VM { param ($Name) }

        It 'Calls Get-VM' {
            Mock Get-VM –ParameterFilter { $Name –eq ‘TestVM } -MockWith { }
            Get-VM -Name 'TestVM';
            Assert-MockCalled Get-VM –ParameterFilter { $Name –eq ‘TestVM’ } -Scope It;
        }

    }
}

The tests will now once again pass successfully!

Describing Missing Cmdlet Mocking Example
 [+] Calls Get-VM 169ms
Tests completed in 169ms
Passed: 1 Failed: 0 Skipped: 0 Pending: 0

Hopefully this helps someone and saves some time. It took me a while to work out what was going on as I had the cmdlets available on my development machine but the tests were failing when running in an Appveyor VM. Perhaps I should submit a pull request to get this put into the Pester help documentation?!

Searching for String Properties with Powershell

I had a requirement recently to parse a configuration file (let’s just say for documentation purposes) and I needed to retrieve a property/value pair which may or may not be present in a text line. Now, depending on the product we wish to document, we might have a line in the configuration file constructed as follows:

set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3

Now, if wanted the “-intfType” value we could split the string using the Powershell .Split() method like so:

PS C:\> $SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
$LineComponents = $SourceString.Split();
$LineComponents[12];
"Xen

However, there are two issues here:

  1. What happens if there are additional or missing properties (the index/order will change)?
  2. We were hoping/expecting to see “Xen Virtual” output and not just “Xen.

Here’s a quick function that will solve issue #1 (complete with case insensitivity):

#############################################################################
#.SYNOPSIS
# Get named property from a string.
#
#.DESCRIPTION
# Returns a case-insensitive property from a string, assuming the property is
# named before the actual property value and is separated by a space. For
# example, if the specified SearchString contained
# "-property1 <value1> -property2 <value2>”, searching # for "-Property1"
# would return "<value1>".
#
#.PARAMETER SearchString
# String to search for the specified property in.
#
#.PARAMETER PropertyName
# The property name to search for.
#
#.PARAMETER Default
# If the property is not found return the specified string. This parameter is
# optional and if not specified returns $null by default.
#
#.EXAMPLE
# $propertyValue = Get-StringProperty $StringToSearch "-property1"
#
#.EXAMPLE
# $propertyValue = Get-StringProperty $StringToSearch "-property3" "Not found"
##############################################################################
function Get-StringProperty([string]$SearchString, [string]$PropertyName, [string]$Default = $null)
{
    # Split the $SearchString based on one or more blank spaces
    $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
    for ($i = 0; $i -le $stringComponents.Length; $i++) {
        # The standard Powershell CompareTo method is case-sensitive
        if ([string]::Compare($stringComponents[$i], $PropertyName, $true) -eq 0) {
            # Check that we're not over the array boundary
            if ($i+1 -le $stringComponents.Length) {
                # If you wanted to trim quotation marks you could use this instead:
                #  return $StringComponents[$i+1].Trim('"');
                return $stringComponents[$i+1];
            }
        }
    }
    # If nothing has been found or we're over the array boundary, return the $EmptyString value
    return $Default;
}

$SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
Get-StringProperty $SourceString "-intftype"

"Xen

Solving issue #2 is a little more involved as we need find the quoted text, escape it and then restore it when needed. The following Get-StringProperty advanced function will first replace all quoted spaces with “^” and then replace the quotes themselves with “^^”. This permits the split function to work as expected. Finally, everything is put back together just before we need it!

<#
.SYNOPSIS
   Get a named property value from a string.
.DESCRIPTION
   Returns a case-insensitive property from a string, assuming the property is
   named before the actual property value and is separated by a space. For
   example, if the specified SearchString contained "-property1 <value1>
   -property2 <value2>”, searching for "-Property1" would return "<value1>".
.PARAMETER SearchString
   String to search for the specified property name.
.PARAMETER PropertyName
   The property name to search the SearchString for.
.PARAMETER Default
   If the property is not found returns the specified string. This parameter is
   optional and if not specified returns $null (by default) if the property is
   not found.
.EXAMPLE
   Get-StringProperty -SearchString $StringToSearch -PropertyName "-property1"

   This command searches the $StringToSearch variable for the presence of the property
   "-property1" and returns its value, if found. If the property name is not found,
   the default $null will be returned.
.EXAMPLE
   Get-StringProperty $StringToSearch "-property3" "Not found"

   This command searches the $StringToSearch variable for the presence of the property
   "-property3" and returns its value, if found. If the property name is not found,
   the "Not Found" string will be returned.
.NOTES
   Author - Iain Brighton - @iainbrighton, iain.brighton@virtualengine.co.uk
#>
function Get-StringProperty {

    [CmdletBinding(HelpUri = 'https://virtualengine.co.uk/2014/searching-for-string-properties-with-powershell/')]
    [OutputType([String])]
    Param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Search")]
        [string]
        $SearchString,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [Alias("Name","Property")]
        [string]
        $PropertyName,

        [Parameter(ValueFromPipelineByPropertyName=$true, Position=2)]
        [AllowNull()]
        [String]
        $Default = $null
    )

    Begin { }

    Process {
        # Locate and replace quotes with '^^' and quoted spaces '^' to aid with parsing, until there are none left
        while ($SearchString.Contains('"')) {
            # Store the right-hand side temporarily, skipping the first quote
            $searchStringRight = $SearchString.Substring($SearchString.IndexOf('"') +1);
            # Extract the quoted text from the original string
            $quotedString = $SearchString.Substring($SearchString.IndexOf('"'), $searchStringRight.IndexOf('"') +2);
            # Replace the quoted text, replacing spaces with '^' and quotes with '^^'
            $SearchString = $SearchString.Replace($quotedString, $quotedString.Replace(" ", "^").Replace('"', "^^"));
        }

        # Split the $SearchString based on one or more blank spaces
        $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
        for ($i = 0; $i -le $stringComponents.Length; $i++) {
            # The standard Powershell CompareTo method is case-sensitive
            if ([string]::Compare($stringComponents[$i], $PropertyName, $True) -eq 0) {
                # Check that we're not over the array boundary
                if ($i+1 -le $stringComponents.Length) {
                    # Restore any escaped quotation marks and spaces
                    # If you wanted to trim quotation marks you could use this instead:
                    #  return $stringComponents[$i+1].Replace("^^", '"').Replace("^", " ").Trim('"');
                    return $stringComponents[$i+1].Replace("^^", '"').Replace("^", " ");
                }
            }
        }
        # If nothing has been found or we're over the array boundary, return the default value
        return $Default;
    }

    End { }
}

$SourceString = 'set interface 1/3 -ifAlias DMZ -throughput 0 -bandwidthHigh 0 -bandwidthNormal 0 -intftype "Xen Virtual" -ifnum 1/3';
Get-StringProperty $SourceString "-intftype"

"Xen Virtual"

That’s much better Smile. Just for good luck I have also created a complimentary Test-StringProperty advanced function that tests whether a property name is present or not. This removes a lot of if ((Get-StringPropertyValue $SearchString “PropertyName”) -ne $null) { Do-Something } calls.

<#
.SYNOPSIS
   Test for a named property value in a string.
.DESCRIPTION
   Tests for the presence of a property value in a string and returns a boolean
   value. For example, if the specified SearchString contained "-property1
   -property2 <value2>”, searching for "-Property1" or "-Property2" would return
   $true, but searching for "-Property3" would return $false
.PARAMETER SearchString
   String to search for the specified property name.
.PARAMETER PropertyName
   The property name to search the SearchString for.
.EXAMPLE
   Test-StringProperty -SearchString $StringToSearch -PropertyName "-property1"

   This command searches the $StringToSearch variable for the presence of the property
   "-property1". If the property name is found it returns $true. If the property name
   is not found, it will return $false.
.NOTES
   Author - Iain Brighton - @iainbrighton, iain.brighton@virtualengine.co.uk
#>
function Test-StringProperty {

    [CmdletBinding(HelpUri = 'https://virtualengine.co.uk/2014/searching-for-string-properties-with-powershell/')]
    [OutputType([bool])]
    Param
    (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [ValidateNotNullOrEmpty()]
        [Alias("Search")]
        [string]
        $SearchString,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, Position=1)]
        [ValidateNotNullOrEmpty()]
        [Alias("Name","Property")]
        [string]
        $PropertyName
    )

    Begin { }

    Process {
        # Split the $SearchString based on one or more blank spaces
        $stringComponents = $SearchString.Split(' +',[StringSplitOptions]'RemoveEmptyEntries'); 
        for ($i = 0; $i -le $stringComponents.Length; $i++) {
            # The standard Powershell CompareTo method is case-sensitive
            if ([string]::Compare($stringComponents[$i], $PropertyName, $True) -eq 0) { return $true; }
        }
        # If nothing has been found or we're over the array boundary, return the default value
        return $false;
    }

    End { }
}

Some final words of warning:

  • If there are escaped (double) quotes then this function won’t work without some modification.
  • If the $SearchString just so happens to natively contain either “^” and/or “^^” it won’t (currently) work either.
  • If you have any improvements or feedback then please let me know.
  • Please test in your environment before putting any 3rd party or external code into production!

RES Automation Manager Quick Tip – appending to existing registry values

I was recently asked (by one of our existing RES Automation Manager customers) how they go about adding to an existing registry value using RES Automation Manager. Well the answer is simple really – by using the @REGISTRY function. I’ll detail how you go about using this function in this blog post.

  1. Firstly start the RES Automation Manager console;
  2. Select “Modules” from the left hand pane, Right Click and select “Add”;
  3. Give the module a suitable name then select the “Tasks” Tab, Right Click and select “Add”.
  4. Select the task “Registry Setting (Apply,Query)” and select “Apply”.
  5. You will now be presented with a dialogue where you can select various methods to add the required registry value you wish to append too. In my example I’m going to APPEND a new string to the START of the existing USERINIT registry value. Select “HKEY_LOCAL_MACHINE” from the left hand pane, Right Click and select “Open HKEY_LOCAL_MACHINE…”.
  6. Browse to “HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon” and select “Userinit”, this will add this value to the current dialog box. image
  7. Now we are going to add the @REGISTRY function to the Userinit value by Right Clicking on “Userinit” in the right hand pane and selecting “Modify”.
  8. In the “Value Data” field, Right Click and select “Insert Functions” >; “@[REGISTRY(;)]”.image
  9. RES Automation Manager now provides you with a nice GUI that allows you to browse to the registry value you wish to retrieve, when the job is executed on the agent. In my case this is going to be the registry value I selected in Step 6, as this is the value I’d like to append too.
  10. Now I simply add the new value that I wish to append, before the @REGISTRY function or after, depending where I’d like my value to appear – in my case this value is “MyNewValuetoAppend” [code]MyNewValuetoAppend,@[REGISTRY(HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon)][/code]
  11. The resulted registry value now looks like this, once the job has been scheduled and completed [code]MyNewValuetoAppend,C:\Windows\system32\userinit.exe[/code]

That’s all there is to it! Smile

Nathan