VS Code + Integrated Console + Pester = No Excuse Testing

Following up on an earlier post where I lightly touch on using the PowerShell Extension for the VS Code editor, I’d love to share how easy it is to Pester test and hopefully convince you to start testing your code today! If you’re not familiar with me, I’m a huge fan of Pester. I mean this is such a great module that it’s baked into Windows! I Pester test EVERYTHING. Even things outside of my custom PowerShell functions… ETL executions, Jenkins job statuses, all kinds of things. I did a post about Pester testing SQL Server stuff, check it out if you haven’t seen it!

Using VS Code and the PowerShell extension, writing Pester tests has never been easier!!


Let’s say we have a semi-logical, dare I say smart(er) function called demo declared and saved to demo.ps1 as such:

integrated_console_pester_tests.png

function demo {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
      [Object]$Config
  )

  # every config object should contain uri & message keys
  if (-not ($config.keys.contains('URI'))) { Throw 'Missing URI Key'}
  if (-not ($config.keys.contains('Message'))) { Throw 'Missing Message Key'}

  Write-Output 'All Good!'
}

Maybe this will be a logging function that posts a message to a logging service. Or maybe it’s going to call a web service for an additional piece of data. Whatever the case may be, we probably want to make sure the script throws an error if a required property is missing.

Create a new file (demo.tests.ps1) and save it next to demo.ps1.

# the directory of demo.tests.ps1
$script_dir =  Split-Path -Parent $MyInvocation.MyCommand.Path

# load the script to test into memory
. "$script_dir\demo.ps1"

Describe 'Demo Function' {
  it 'throws an error when $config does not contain a URI key' {
    $params = @{
      notmykey = 'value'
    }
    { $params | demo } | Should Throw 'Missing URI Key'
  }

  it 'throws an error when $config does not contain a Message key' {
    $params = @{
      URI = 'https://demo'
    }

    { $params | demo } | Should Throw 'Missing Message Key'
  }

  it 'passes validation' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo | Should Be "All Good!"
  }
}

And I can just hit F5 anywhere in the document!?

interactive_console_pester_1

WOW!!

So that’s super easy, I can write and update tests right in VS Code.

Extra Credit!

Let’s actually write and test the Invoke-RestMethod call to demo how Pester can intercept calls and inspect content.

I’ll change my demo.ps1 function to build a basic JSON body to pass along in the POST method. I’ve also included the functionality to have dynamic root level properties, completely optional added to the request body.

function demo {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
      [Object]$Config
  )

  # every config object should contain uri & message keys
  if (-not ($config.keys.contains('URI'))) { Throw 'Missing URI Key'}
  if (-not ($config.keys.contains('Message'))) { Throw 'Missing Message Key'}

  # I'm just going to build a json body with the message in it.
  $body = @{
    Message = $Config.Message
  }

  # $config can have additional/optional properties
  # tack them to the body
  if ($Config.Keys.Count -gt 2) {
    $Config.Keys.Where({$_ -ne 'URI' -and $_ -ne 'Message'}) |
      %{ $body.add($_,$Config["$_"]) }
  }

  Invoke-RestMethod -Uri $Config.URI -Method Post -Body $($body | ConvertTo-Json -Depth 99) -UseBasicParsing
}

Now for the test, I’ll mock out the Invoke-RestMethod which will give me some incredible ability to test what my function actually passes to Invoke-RestMethod

 beforeEach {
    Mock Invoke-RestMethod { }
  }

I like to put my mocks in a beforeEach { } block as it allows me to change the mock’s functionality in one of my tests and resume its basic functionality for the next test. I’ll demo that a bit below.

Right away, I can just verify that my mock is actually being called. I’ll rely on theAssert-MockCalled command to verify this.

  it 'the function calls my mocked declaration' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo

    Assert-MockCalled 'Invoke-RestMethod' -Exactly 1 -Scope It
  }

Ok, neat but not really useful right? I agree, but using Pester, I can inspect the parameter values that were sent to the Invoke-RestMethod command.

  it 'the mocks parameters are now testable!' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo

    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$URI -eq 'https://demo' } -Exactly 1 -Scope It
    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$Body -match 'This is a test' } -Exactly 1 -Scope It
  }

 

This is pretty powerful stuff already and I’ve barely scratched the surface of what can be done with Pester.

One More Trick (for the road):

Let’s make sure that my function ( demo  ) does in fact build a JSON body. I’m going to declare a new mock definition for Invoke-RestMethod but this time, I’m going to write the content of the $Body parameter to stdout.


  it 'i can change the functionality of a mock to do pretty much anything' {

    # change Invoke-RestMethod to write the contents of $Body to stdout
    Mock Invoke-RestMethod {
      Write-Output $Body
    }

    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
      NewProperty1 = 'Value'
      NewProperty2 = 1
    }

    # store the output
    $output = $params | demo

    # Look, I can convert this to JSON
    $output_converted = $output | ConvertFrom-Json

    # and test the individual properties of the body!
    $output_converted.Message | Should Be 'This is a test'
    $output_converted.NewProperty1 | Should Be 'Value'
    $output_converted.NewProperty2 | Should Be 1
  }

Remember declaring the Invoke-RestMethod in the beforeEach { } block? Well, now I can go back to using the original Invoke-RestMethod mock.

it 'the original mock is used' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo | Should Be $null

    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$URI -eq 'https://demo' } -Exactly 1 -Scope It
    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$Body -match 'This is a test' } -Exactly 1 -Scope It
  }

I mean, seriously how cool is this stuff!? I was able to dynamically change the functionality of Invoke-RestMethod, I could have injected whatever logic I needed to validate whatever I need to validate. That logic is then executed in place of the real Invoke-RestMethod that is called in the demo function. If I set these tests to run during a code pipeline, I can make sure no one breaks this in the future.

I hope you enjoyed! As always, comments/feedback/questions are always appreciated!

The entire test file:

# the directory of demo.tests.ps1
$script_dir =  Split-Path -Parent $MyInvocation.MyCommand.Path

# load the script to test into memory
. "$script_dir\demo.ps1"

Describe 'Demo Function' {
  beforeEach {
    Mock Invoke-RestMethod { }
  }

  it 'throws an error when $config does not contain a URI key' {
    $params = @{
      notmykey = 'value'
    }
    { $params | demo } | Should Throw 'Missing URI Key'
  }

  it 'throws an error when $config does not contain a Message key' {
    $params = @{
      URI = 'https://demo'
    }

    { $params | demo } | Should Throw 'Missing Message Key'
  }

  it 'the function calls my mocked declaration' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo

    Assert-MockCalled 'Invoke-RestMethod' -Exactly 1 -Scope It
  }

  it 'the mocks parameters are now testable!' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo

    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$URI -eq 'https://demo' } -Exactly 1 -Scope It
    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$Body -match 'This is a test' } -Exactly 1 -Scope It
  }

  it 'i can change the functionality of a mock to do pretty much anything' {

    # change Invoke-RestMethod to write the contents of $Body to stdout
    Mock Invoke-RestMethod {
      Write-Output $Body
    }

    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
      NewProperty1 = 'Value'
      NewProperty2 = 1
    }

    # store the output
    $output = $params | demo

    # Look, I can convert this to JSON
    $output_converted = $output | ConvertFrom-Json

    # and test the individual properties of the body!
    $output_converted.Message | Should Be 'This is a test'
    $output_converted.NewProperty1 | Should Be 'Value'
    $output_converted.NewProperty2 | Should Be 1
  }

  it 'the original mock is used' {
    $params = @{
      URI = 'https://demo'
      Message = 'This is a test'
    }

    $params | demo | Should Be $null

    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$URI -eq 'https://demo' } -Exactly 1 -Scope It
    Assert-MockCalled 'Invoke-RestMethod' -ParameterFilter {$Body -match 'This is a test' } -Exactly 1 -Scope It
  }
}

2 comments

Leave a comment