Mocking .NET Objects in Pester Scripts

You have written several functions in a PowerShell script or module. You want to use Pester, a unit testing framework for PowerShell, to create unit tests for those functions. Unfortunately, several of the functions call methods of .NET objects, and you can’t mock those method calls. As result, the method is executed when you run your test script.

To illustrate the fix we’ll use the following example:

function do-thing {
    Param ($servername)

    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.smo") `
        | Out-Null

    $server = New-Object 'Microsoft.SqlServer.Management.Smo.Server' `
        -ArgumentList $servername

    $Server.ConnectionContext.ConnectTimeout = 5
    $Server.ConnectionContext.Connect()
    $server.Databases
}

When `do-thing` is executed the following occurs:

  1. Microsoft.SqlServer.Management.Smo.Server object named $server is created
  2. The $server object’s Connect() method is called
  3. The $server object’s database collection is returned

This function will blow-up at step 2 unless its actually connecting to a live SQL Server instance, an external element. It’s a generally accepted best-practice that unit tests should be independent of external elements, components outside the unit being tested. You typically control interactions with external components through the use of mocks.

If you’re running PowerShell 5.x or greater, you can employ a mock object to solve this problem. A mock object according to Margaret Rouse “…is a simulated object that mimics the behavior of the smallest testable parts of an application in controlled ways.” You’ll start by creating a mock class, which you’ll instantiate into a mock object. The following is an example of this concept in practice:

describe 'do-thing' {
    class fake_smo_connnection_context {
        [int] $ConnectTimeout
        [void] connect(){ }
    }

    class fake_smo_server {
        [string[]] $databases = @('foo', 'bar')
        [fake_smo_connnection_context] $ConnectionContext

        fake_smo_server() {
            $this.ConnectionContext = `
                New-Object 'fake_smo_connnection_context'
        }
    }

    context 'when the server exists' {

        Mock 'New-Object' { New-Object 'fake_smo_server' } `
            -ParameterFilter {
                $TypeName -and  
                $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server'
            }

        it 'should complete successfully' {
            do-thing -servername 'whatever'
        }

        it 'should return databases' {
            $databases = do-thing -servername 'whatever'
            $databases.Count | Should -Be 2
        }
    }

    context 'when the server does not exist' {

        it 'should throw an exception' {
            { do-thing -servername 'whatever' } | Should -Throw
        }
    }
}

We’ve created two classes:

  1. fake_smo_connnection_context, a class to mock the server’s ConnectionContext object
  2. fake_smo_server, a class to mock the server object

fake_smo_connnection_context has single method named Connect() that does nothing and a single property ConnectTimeout. fake_smo_connnection_context mocks the ConnectionContext type. We’re not creating a complete facsimile of the type we want to mimic. We’re only creating the parts of the server object needed to unit test this function. Since fake_smo_server contains fake_smo_connnection_context we’re declaring fake_smo_connnection_context before fake_smo_server. We do this because you can’t create a variable of type fake_smo_connnection_context until type fake_smo_connnection_context has been defined. Lastly, the classes were defined in a describe block. We did this because the context and it child-blocks have visibility into the parent describe block. I tend to define mock-classes one scope up from where I’ll use them.

If you’re familiar with classes but aren’t familiar with them in PowerShell, checkout the post PowerShell v5 Classes & Concepts by Michael Willis. If you’re not familiar with classes at all, …I’m impressed that you read this far. You’ll want to search around for an introduction to classes in PowerShell. However, if you’re in a pinch there is an alternative.

If you can’t create a mock object, you can wrap the method call in a PowerShell function then simply not test the wrapper. The following is the function with a wrapper:

function run-wrapper {
    Param ($server)

    $Server.ConnectionContext.ConnectTimeout = 5
    $Server.ConnectionContext.Connect()
    $server.Databases
}
function do-thing {
    Param ($servername)

    [Reflection.Assembly]::LoadWithPartialName("Microsoft.SqlServer.smo") `
        | Out-Null

    $server = New-Object 'Microsoft.SqlServer.Management.Smo.Server' `
        -ArgumentList $servername

    run-wrapper -Server $server
}

The code in this post was tested with Pester 4.4.0 and PowerShell 5.1.17134. Pester changed the way assertions were passed with Pester 4.x. With Pester 4.x assertions are passed as parameters. If you are running Pester 3.x, some of the code in this article will not work.