I noticed something peculiar when using Should -Invoke
on a Mock command. When running without a Break
statement in the Code Block, Should -Invoke
works perfectly fine; but when running with a Break
statement, Should -Invoke
will always validate as TRUE, that is, Pester will pass all tests.
I do not want to remove Break
statement from my original .ps1
script as I need this. Therefore, I would like to know if there is a way that I can Mock Break
statement using pester or if there are any workaround for this, so that i can continue to validate all Mock commands without removing Break
statement from my source code.
Below is the code that I used to show the behavior of Break
statement given in the else
code block when running with Pester. Notice that Context VALIDATE ELSE BLOCK
passes all tests.
BeforeAll {
function Something-Function {
Param(
[Parameter(Mandatory)][String]$SamplePath
)
Write-Host "Hello from Function"
if (Test-Path -Path $SamplePath -PathType Leaf) {
Write-Host "Hello from If"
"Hello Earth!" | Out-File $SamplePath -Encoding utf8 -Append
}
else {
Write-Host "Hello from Else"
"Hello World!!" | Out-File $SamplePath -Encoding utf8 -Append
Break
}
}
}
Describe "Something-Function" {
BeforeAll {
Mock Write-Host {}
$TestPath = "TestDrive:\"
$FileName = "fake.txt"
}
Context "VALIDATE IF BLOCK" {
BeforeAll {
Mock Test-Path -MockWith {return $true}
# New-Item -ItemType File -Path $($TestPath + $FileName) -Force
"Hello World!!" | Out-File $($TestPath + $FileName) -Encoding utf8 -Append
}
It "Should -Invoke Write-Host -Exactly -Times 1" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 1
}
It "Should -Invoke Write-Host -Exactly -Times 2" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 2
}
}
Context "VALIDATE ELSE BLOCK" {
BeforeAll {
Mock Test-Path -MockWith {return $false}
}
It "Should -Invoke Write-Host -Exactly -Times 1" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 1
}
It "Should -Invoke Write-Host -Exactly -Times 2" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 2
}
It "Should -Invoke Write-Host -Exactly -Times 3" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 3
}
It "Should -Invoke Out-File -Exactly -Times 50" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Write-Host -Exactly -Times 50
}
It "Should -Invoke Import-Csv -Exactly -Times 100" {
Something-Function -SamplePath $($TestPath + $FileName)
Should -Invoke Import-Csv -Exactly -Times 100
}
}
}
# PS C:\user\repo> Invoke-Pester -Output Detailed .\TEST\Issue.tests.ps1
# Pester v5.3.3
# Starting discovery in 1 files.
# Discovery found 7 tests in 199ms.
# Running tests.
# Running tests from 'C:\user\repo\TEST\Issue.tests.ps1'
# Describing Something-Function
# Context VALIDATE IF BLOCK
# [-] Should -Invoke Write-Host -Exactly -Times 1 96ms (93ms|3ms)
# Expected Write-Host to be called 1 times exactly but was called 2 times
# at Should -Invoke Write-Host -Exactly -Times 1, C:\user\repo\TEST\Issue.tests.ps1:42
# at <ScriptBlock>, C:\user\repo\TEST\Issue.tests.ps1:42
# [+] Should -Invoke Write-Host -Exactly -Times 2 34ms (34ms|0ms)
# Context VALIDATE ELSE BLOCK
# [+] Should -Invoke Write-Host -Exactly -Times 1 69ms (66ms|3ms)
# [+] Should -Invoke Write-Host -Exactly -Times 2 35ms (34ms|0ms)
# [+] Should -Invoke Write-Host -Exactly -Times 3 33ms (33ms|0ms)
# [+] Should -Invoke Out-File -Exactly -Times 50 35ms (34ms|0ms)
# [+] Should -Invoke Import-Csv -Exactly -Times 100 35ms (34ms|1ms)
# Tests completed in 739ms
# Tests Passed: 6, Failed: 1, Skipped: 0 NotRun: 0
I have try mocking Break
statement Mock Break {}
but failed and generate the below error.
[-] Context Something-Function.VALIDATE ELSE BLOCK failed
CommandNotFoundException: Could not find Command Break
If you follow the link in @mklement0's comment above you'll see this:
Do not use break outside of a loop, switch, or trap
When break is used outside of a construct that directly supports it (loops, switch, trap), PowerShell looks up the call stack for an enclosing construct. If it can't find an enclosing construct, the current runspace is quietly terminated.
This means that functions and scripts that inadvertently use a break outside of an enclosing construct that supports it can inadvertently terminate their callers.
Using break inside a pipeline break, such as a ForEach-Object script block, not only exits the pipeline, it potentially terminates the entire runspace.
The following bad code demonstrates this in action:
#
# BAD CODE FOLLOWS - DON'T DO THIS !!!
#
function Test-Outer
{
$x = 1
write-host "entering outer"
switch( $x )
{
1 {
write-host "outer before"
Test-Inner $x
write-host "outer after"
}
}
write-host "leaving outer"
}
function Test-Inner
{
param( $y )
if( $y -eq 1 )
{
write-host "inner before"
# DON'T DO THIS OUTSIDE OF A LOOP, SWITCH OR TRAP !!!
# (the 'break' will bubble up the call stack and terminate the switch case in Test-Outer)
break;
write-host "inner after"
}
}
Test-Outer
which gives this output:
entering outer
outer before
inner before
leaving outer
Note that we don't see inner after
or outer after
in the output - in short, the break
inside the Test-Inner
actually terminates the switch
case inside Test-Outer
and execution skips to the code after the switch
block.
You're effectively controlling the flow of the calling code from within Test-Inner
, which might work for your current script but it breaks the encapsulation of functions and as you can see from the documentation it's not recommended. A side effect is that trying to test Test-Inner
with Pester results in Test-Inner
unexpectedly terminating the calling Pester test harness...
A better approach might be for your Test-Outer
function to decide whether to call break
to control its own flow based on a result returned from Test-Inner
:
function Test-Outer
{
$x = 1
write-host "entering outer"
switch( $x )
{
1 {
write-host "outer before"
if( -not (Test-Inner $x) )
{
break;
}
write-host "outer after"
}
}
write-host "leaving outer"
}
# returns $true for success or $false for failure
function Test-Inner
{
param( $y )
if( $y -eq 1 )
{
write-host "inner before"
return $false
write-host "inner after"
}
return $true
}
Test-Outer
We still see the same output from the second example...
entering outer
outer before
inner before
leaving outer
... but now the control flow (break
) is local to the code where the control statements (switch
) live and you can call Test-Inner
without the break
bubbling up to the caller which makes it safe to execute from other callers (e.g. Pester).
If you really can't live with moving the break
out of Test-Inner
you could wrap the call in a sacrificial control statement so that gets terminated rather than the main caller code.
For example, this version of Test-Outer
doesn't exit the switch
case when Test-Inner
invokes break
because it terminates the sacrificial for
instead...
function Test-Outer
{
$x = 1
write-host "entering outer"
switch( $x )
{
1 {
write-host "outer before"
# run *one* iteration, but also act as a sacrificial
# target for the effect of the "break" in Test-Inner
for( $i = 0; $i -lt 1; $i++ ) { Test-Inner $x }
write-host "outer after"
}
}
write-host "leaving outer"
}
This now outputs the following:
entering outer
outer before
inner before
outer after
leaving outer
...but it means you'll have to add the workaround at every call-site - for example in your Pester tests it would become this:
It "Should -Invoke Write-Host -Exactly -Times 1" {
for( $i = 0; $i -lt 1; $i++ ) {
Something-Function -SamplePath $($TestPath + $FileName)
}
Should -Invoke Write-Host -Exactly -Times 1
}
which will get messy very quickly, and overall it's probably better to invest time in cleaning up your script rather than making more convoluted tests to work around the funky break
in the main script...