I was minding my own business, when Donovan Brown (twitter, blog) DM’d me on Microsoft Teams to ask for a second pair of eyes to troubleshoot a test case that was failing.

The tests were for a command in the VSTeam module. There were three tests in a Context block. Each test had a Mock inside of their It block. The exact tests didn’t matter so much, but I’ve included them below to give you an idea of what the structure looked like.

Debugging the test

When we ran the tests, the third test in the block would fail by reporting that the mock of Invoke-RestMethod was called twice. To debug the test, we did a few things:

  • First, we shuffled them around and it was always the second or third test that failed with the same error
  • We looked at the function definitions and any functions those called and talked through the logic
  • We added logging in the mock to see how often it was being called as the tests ran (it was firing once per test)
  • We changed the mock definition from inside the It to the Context
  • We looked at the documentation for Assert-MockCalled

After none of that seemed to shed any new light, I suggested putting a Write-Host "Assert-Mock with $uri line in the parameter filter of the Assert-MockCalled of the failing test. This led to us seeing each test evaluated all the previous Invoke-RestMethod calls within the Context. For the first test, we saw one URI echoed out. For the second, we saw two. And then for the third, we saw three.

Why were we only seeing an error for the mock being called twice? Well, two of the tests construct web requests with the same parameters - so it would always fail on that second test. The one test that had differen parameters just added uncertainty to the experience.

Afterward

Once we had a good understanding of what was happening, we looked deeper into the docs and GitHub project to see if we could find anything that documented that behavior.

There was nothing in about_Mocking and the docs are in transition from the GitHub wiki to pester.dev. In the wiki, sections about Describe and Context both state that when they go out of scope, any mocks inside are cleaned up. There’s no such claim in the It scope. The help for Assert-MockCalled and related Assert-’s did not shed any light either.

I did find a line at the start of the help for Mock that was definitive.

This creates new behavior for any existing command within the scope of a Describe or Context block.

Results

There are two main results from our experience.

First, pair programming (more specifically, pair debugging) is a wonderful thing. We did this over Microsoft Teams while self-quarantining in our own residences (we also live in different states). Getting a fresh pair of eyes on a problem can help as the primary developer walks the new person through what is happening, revealing assumptions and uncovering hidden dependencies.

Second, Pester needs updated documentation on the topic. We aren’t the only ones who have hit this issue - there’s an issue filed from the end of 2019 as well. It’s an open source project maintained by volunteers, so if you’ve got some spare time, maybe this would be a great way to get involved with a project that fills a real need in the PowerShell community.

Reference

The context block we were troubleshooting looked like:

Context 'Add-VSTeamAccessControlEntry' {

  Set-VSTeamDefaultProject -Project Testing

  It 'By SecurityNamespaceId Should return ACEs' {
    Mock Invoke-RestMethod {
        return $accessControlEntryResult
    }

    Add-VSTeamAccessControlEntry -SecurityNamespaceId 5a27515b-ccd7-42c9-84f1-54c998f03866 -Descriptor abc -Token xyz -AllowMask 12 -DenyMask 15

    Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {
      $Uri -like "https://dev.azure.com/test/_apis/accesscontrolentries/5a27515b-ccd7-42c9-84f1-54c998f03866*" -and
      $Uri -like "*api-version=$([VSTeamVersions]::Core)*" -and
      $Body -like "*`"token`": `"xyz`",*" -and
      $Body -like "*`"descriptor`": `"abc`",*" -and
      $Body -like "*`"allow`": 12,*" -and
      $Body -like "*`"deny`": 15,*" -and
      $ContentType -eq "application/json" -and
      $Method -eq "Post"
    }
  }

  It 'By SecurityNamespace Should return ACEs' {
    Mock Get-VSTeamSecurityNamespace { return $securityNamespaceObject }
    Mock Invoke-RestMethod { return $accessControlEntryResult } -Verifiable

    $securityNamespace = Get-VSTeamSecurityNamespace -Id "58450c49-b02d-465a-ab12-59ae512d6531"
    Add-VSTeamAccessControlEntry -SecurityNamespace $securityNamespace -Descriptor abc -Token xyz -AllowMask 12 -DenyMask 15

    Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {
      $Uri -like "https://dev.azure.com/test/_apis/accesscontrolentries/58450c49-b02d-465a-ab12-59ae512d6531*" -and
      $Uri -like "*api-version=$([VSTeamVersions]::Core)*" -and
      $Body -like "*`"token`": `"xyz`",*" -and
      $Body -like "*`"descriptor`": `"abc`",*" -and
      $Body -like "*`"allow`": 12,*" -and
      $Body -like "*`"deny`": 15,*" -and
      $ContentType -eq "application/json" -and
      $Method -eq "Post"
    }
  }

  It 'By SecurityNamespace (pipeline) Should return ACEs' {
    Mock Get-VSTeamSecurityNamespace { return $securityNamespaceObject }
    Mock Invoke-RestMethod { return $accessControlEntryResult } -Verifiable

    Get-VSTeamSecurityNamespace -Id "58450c49-b02d-465a-ab12-59ae512d6531" | 
      Add-VSTeamAccessControlEntry -Descriptor abc -Token xyz -AllowMask 12 -DenyMask 15

    Assert-MockCalled Invoke-RestMethod -Exactly 1 -ParameterFilter {
      $Uri -like "https://dev.azure.com/test/_apis/accesscontrolentries/58450c49-b02d-465a-ab12-59ae512d6531*" -and
      $Uri -like "*api-version=$([VSTeamVersions]::Core)*" -and
      $Body -like "*`"token`": `"xyz`",*" -and
      $Body -like "*`"descriptor`": `"abc`",*" -and
      $Body -like "*`"allow`": 12,*" -and
      $Body -like "*`"deny`": 15,*" -and
      $ContentType -eq "application/json" -and
      $Method -eq "Post"
    }
  }
}