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 theContext
- 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"
}
}
}