Archive

Archive for the ‘TDDSeams’ Category

PowerShell TDD: Testing ShouldProcess

July 12th, 2022 No comments

The built-in PowerShell feature ShouldProcess is considered best practice for commands that perform destructive operations.

function Remove-Something {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    if ($PSCmdlet.ShouldProcess("Something", "Permanently remove")) {
        # Code that performs the destructive operation
    }
}

The problem with ShouldProcess is that it may prompt the user for manual confirmation, which makes automated testing difficult or impractical. The normal approach to automate testing of processes that involves manual prompting is to mock the part which performs it. However, since ShouldProcess is a .NET method on an automatic context variable ($PSCmdlet), testing frameworks like Pester aren’t able to replace its implementation with a mock.

Fortunately, there is a way to make ShouldProcess adhering commands fully testable. It just requires a little bit of extra work, such as wrapping ShouldProcess into a PowerShell function of its own.

Wrapping ShouldProcess

Here’s a PowerShell function that wraps the ShouldProcess method and makes it possible to use mocks to bypass the prompting.

function Invoke-ShouldProcess {
    # Suppress the PSShouldProcess warning since we're wrapping ShouldProcess to be able to mock it from a Pester test.
    # Info on suppressing PSScriptAnalyzer rules can be found here:
    # https://github.com/PowerShell/PSScriptAnalyzer/blob/master/README.md#suppressing-rules
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.Management.Automation.PSCmdlet]
        $Context,
        [Parameter(Mandatory)]
        [string]
        $Target,
        [Parameter(Mandatory)]
        [string]
        $Operation,
        [string]
        $Message
    )
    if ($Message) {
        $Context.ShouldProcess($Message, $Target, $Operation)
    } else {
        $Context.ShouldProcess($Target, $Operation)
    }
}

Actually, there are four overloads of the ShouldProcess method, but the code above only handles the two most useful ones (in my opinion).

The next step is to replace the ShouldProcess method in the command, and use the wrapper instead.

function Remove-Something {
    [CmdletBinding(SupportsShouldProcess)]
    param()
    if (Invoke-ShouldProcess -Context $PSCmdlet -Target "Something" -Operation "Permanently remove") {
        # Code that performs the destructive operation
    }
}

Note that you need to provide the command’s $PSCmdlet variable as the context on which ShouldProcess should be invoked.

Mocking Invoke-ShouldProcess

Now our command is ready for some Pester testing. Here are a couple of test stubs that show you how to mock the wrapper. Hopefully, you’ll be able to take it from here and start testing your own ShouldProcess-aware commands.

It 'Should perform the operation if the user confirms' {
    Mock Invoke-TDDShouldProcess { $true }

    Remove-Something
        
    # Code that verifies that the operation was executed
}

It 'Should not perform the operation if the user aborts' {
    Mock Invoke-TDDShouldProcess { $false }

    Remove-Something

    # Code that verifies that the operation was not executed
}

Introducing TDDSeams

I have created a PowerShell module named TDDSeams which has wrappers for ShouldProcess and its sister ShouldContinue, as well as a couple of helper methods that implement the best practice behavior described by Kevin Marquette in his excellent post Everything you wanted to know about ShouldProcess.

You can install the TDDSeams module from an elevated PowerShell with the following command.

Install-Module TDDSeams

Also, if you are interested in test-driven PowerShell development, check out my previous post PowerShell TDD: Testing CmdletBinding and OutputType.