Pester having problems with else statement - powershell

Hey guys so I have this code (Powershell v5), and when I run the pester for it, the else statement is not being detected. Runs perfectly fine on powershell v7 but need it to run on v5.
Source Code
Function 'INeedHelp'
$array= [Systems.Collections.ArrayList]::new()
$GettingInfo= #Function that's calling an object with properties
$String= 'Example'
$x= $null
$x= $GettingInfo | Where-Object { $._item -like "*$String*" -or $._ $thing like
"*$String*"}
if($x){
$array.Add($x)
if($array.thing -like *Example*) {
$array | Add-Member -membertype NoteProperty -Name Match -Value $true
}
}
else {
return 'fail'
}
Pester
Mock 'Function' {
return[PSCustomObject]#(
item = 'hello'
thing= 'bye'
)
}
It 'Should be able to get the failure result'
$Result= INeedHelp
$Result | Should -be 'fail'
The error is expected 'fail' but got #{item= 'hello'; thing= 'bye'}

You are Mocking the INeedHelp function so that it always returns an object with two properties. You'll never get a test to therefore return fail because your test will always invoke the Mock and this only returns one result.
If the purpose of your test is to test the INeedHelp function, then Mocking it is the wrong approach as the real code for the function never gets executed.
Instead, based on the code you've put above (which has quite a few syntax errors FYI) you want to be creating a Mock of whatever the function is that returns values to the $GettingInfo variable. You can have different Mocks of this function return different values at different points of your test script and in doing so can ensure that there are tests that enter both parts of the function's if statement.
Here's a working example to show you what I mean:
function Get-InfoFunction {}
function Example {
$GettingInfo = Get-InfoFunction
if ($GettingInfo -is [string]) {
return 'string'
}
else {
return 'fail'
}
}
Describe 'MyTests' {
Context 'String input' {
BeforeAll {
Mock Get-InfoFunction { Return 'hello' }
}
It 'Should return string' {
Example | Should -Be 'string'
}
}
Context 'Non-string input' {
BeforeAll {
Mock Get-InfoFunction { Return 7 }
}
It 'Should return fail' {
Example | Should -Be 'fail'
}
}
}

Related

How can I verify the value of a parameter being passed into a mocked function using Pester

I have a function written in PowerShell and I am trying to test that function using Pester v-5. I have included the function under test and the Pester test case in the code samples below.
I am trying to verify that the parameter $FileName being passed into the mocked function Save-Report is correct. How can I do that?
Notes:
I realise the value of that $Filename parameter is being set to a test value when I mock the function Add-StringToFileName and return a test value. But I still want to test that the value makes it down to the end and remains unchanged.
This is definitely something that I would like to do in future tests so I want to understand how it can be done.
Function under test
Function Invoke-ReportDownloader
{
[CmdletBinding()]
Param
(
# The command object used to connect to the database
[Parameter(Mandatory)]
[System.Data.Odbc.OdbcCommand]$Command,
# An XML element containing the report item to be retrieved
[Parameter(Mandatory)]
[System.Xml.XmlElement]$ReportItem,
# Hashtable containing the configuration parameters
[Parameter(Mandatory)]
[hashtable]$ConfigParams,
# The SEMO participant that the report is being downloaded for
[Parameter(Mandatory)]
[System.Data.DataRow]$Participant
)
# If report is not supported jump to next report
$ReportName = $ReportItem.ReportName
if (-not (Test-ReportSupported $Command $ReportName)) {
$Logger.Warning("Report: $($ReportName) is not currently supported")
return
}
# (Kanban-7940) If there is no date included in the specified file name in
# the report item then we append the associated date to the filename. This
# is to avoid multiple reports of the same type overwriting each other.
$Filename = $ReportItem.FileName
if (-not (Test-TimestampIncluded -Filename $Filename)) {
$FileName = Add-StringToFileName -Path $Filename -SubString $ReportItem.Date
}
# If report already exists in database jump to next report
if (Test-MarketReportInDatabase $Command $ReportItem) {
return
}
Get-Report $ConfigParams $Participant $ReportItem
# If the response contains failure information throw an exception
$ReportSuccessParams = #{
ResponsePath = $ConfigParams.BmResponsePath
BaseName = $ConfigParams.ReportFilter
}
if (-not (Test-SuccessfulResponse #ReportSuccessParams)) {
throw "Unsuccessful response from BM"
}
$SaveReportParams = #{
RawPath = $ConfigParams.RawPath
XmlResponsePath = $ConfigParams.XmlResponsePath
Filename = $FileName
}
Save-Report #SaveReportParams
}
Unit tests
Describe 'Invoke-ReportDownloader' {
BeforeAll {
# Set up test $Cmd object
# Set up test $Logger object
# Set up test $ReportItem object
# Set up test $ConfigParams object
}
Context "No timestamp included in the filename" {
BeforeAll {
Mock Test-ReportSupported { return $true }
Mock Test-MarketReportInDatabase { return $false }
Mock Get-Report
Mock Test-SuccessfulResponse { return $true }
Mock Save-Report
}
BeforeEach {
$Script:ReportDownloaderParams = #{
Command = $Script:Cmd
ReportItem = $ReportItem
ConfigParams = $Script:ConfigParams
Participant = $Script:Participant
}
}
It "Should save the report with the timestamp included in the filename" {
$TestFileName = "test_string_20210101"
Mock Add-StringToFileName { return $TestFileName }
Invoke-ReportDownloader #Script:ReportDownloaderParams
Should -InvokeVerifiable Save-Report
# I want to verify here that the parameter $FileName being passed into the mocked function Save-Report is correct.
}
}
}
You can confirm a mock was called with specific parameters as follows:
Should -Invoke -CommandName Save-Report -Times 1 -ParameterFilter { $Filename -eq "test_string_20210101" }
See https://pester-docs.netlify.app/docs/usage/mocking#example for more details.

Show content of hashtable when Pester test case fails

Problem
When a Hashtable is used as input for Should, Pester outputs only the typename instead of the content:
Describe 'test' {
It 'test case' {
$ht = #{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
}
Output:
Expected $null or empty, but got #(System.Collections.Hashtable).
Expected output like:
Expected $null or empty, but got #{ foo = 21; bar = 42 }.
Cause
Looking at Pester source, the test input is formatted by private function Format-Nicely, which just casts to String if the value type is Hashtable. This boils down to calling Hashtable::ToString(), which just outputs the typename.
Workaround
As a workaround I'm currently deriving a class from Hashtable that overrides the ToString method. Before passing the input to Should, I cast it to this custom class. This makes Pester call my overridden ToString method when formatting the test result.
BeforeAll {
class MyHashTable : Hashtable {
MyHashTable( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
}
Describe 'test' {
It 'test case' {
$ht = #{ foo = 21; bar = 42 }
[MyHashTable] $ht | Should -BeNullOrEmpty
}
}
Now Pester outputs the Hashtable content in JSON format, which is good enough for me.
Question
Is there a more elegant way to customize Pester output of Hashtable, which doesn't require me to change the code of each test case?
Somewhat of a hack, override Pester's private Format-Nicely cmdlet by defining a global alias of the same name.
BeforeAll {
InModuleScope Pester {
# HACK: make private Pester cmdlet available for our custom override
Export-ModuleMember Format-Nicely
}
function global:Format-NicelyCustom( $Value, [switch]$Pretty ) {
if( $Value -is [Hashtable] ) {
return $Value | ConvertTo-Json
}
# Call original cmdlet of Pester
Pester\Format-Nicely $Value -Pretty:$Pretty
}
# Overrides Pesters Format-Nicely as global aliases have precedence over functions
New-Alias -Name 'Format-Nicely' -Value 'Format-NicelyCustom' -Scope Global
}
This enables us to write test cases as usual:
Describe 'test' {
It 'logs hashtable content' {
$ht = #{ foo = 21; bar = 42 }
$ht | Should -BeNullOrEmpty
}
It 'logs other types regularly' {
$true | Should -Be $false
}
}
Log of 1st test case:
Expected $null or empty, but got #({
"foo": 21,
"bar": 42
}).
Log of 2nd test case:
Expected $false, but got $true.
A cleaner (albeit more lengthy) way than my previous answer is to write a wrapper function for Should.
Such a wrapper can be generated using System.Management.Automation.ProxyCommand, but it requires a little bit of stitchwork to generate it in a way that it works with the dynamicparam block of Should. For details see this answer.
The wrappers process block is modified to cast the current pipeline object to a custom Hashtable-derived class that overrides the .ToString() method, before passing it to the process block of the original Should cmdlet.
class MyJsonHashTable : Hashtable {
MyJsonHashTable ( $obj ) : base( $obj ) {}
[string] ToString() { return $this | ConvertTo-Json }
}
Function MyShould {
[CmdletBinding()]
param(
[Parameter(Position=0, ValueFromPipeline=$true, ValueFromRemainingArguments=$true)]
[System.Object]
${ActualValue}
)
dynamicparam {
try {
$targetCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function, $PSBoundParameters)
$dynamicParams = #($targetCmd.Parameters.GetEnumerator() | Microsoft.PowerShell.Core\Where-Object { $_.Value.IsDynamic })
if ($dynamicParams.Length -gt 0)
{
$paramDictionary = [Management.Automation.RuntimeDefinedParameterDictionary]::new()
foreach ($param in $dynamicParams)
{
$param = $param.Value
if(-not $MyInvocation.MyCommand.Parameters.ContainsKey($param.Name))
{
$dynParam = [Management.Automation.RuntimeDefinedParameter]::new($param.Name, $param.ParameterType, $param.Attributes)
$paramDictionary.Add($param.Name, $dynParam)
}
}
return $paramDictionary
}
} catch {
throw
}
}
begin {
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Pester\Should', [System.Management.Automation.CommandTypes]::Function)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process {
try {
# In case input object is a Hashtable, cast it to our derived class to customize Pester output.
$item = switch( $_ ) {
{ $_ -is [Hashtable] } { [MyJsonHashTable] $_ }
default { $_ }
}
$steppablePipeline.Process( $item )
} catch {
throw
}
}
end {
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
To override Pesters Should by the wrapper, define a global alias like this:
Set-Alias Should MyShould -Force -Scope Global
And to restore the original Should:
Remove-Alias MyShould -Scope Global
Notes:
I have also changed the argument of GetCommand() from Should to Pester\Should to avoid recursion due to the alias. Not sure if this is actually necessary though.
A recent version of Pester is required. Failed with Pester 5.0.4 but tested successfully with Pester 5.1.1.

Strange behaviour of string comparison in Powershell

Consider the following function :
function myfunc()
{
if (condition1) {
return 'nowork'
} elseif (condition2) {
return $false
} elseif (condition3) {
return $true
}
Now if I call this function, and I know that condition3 is true, I can see that True is returned:
...
$result = myfunc
Write-Host $result
(this writes True to the console.)
The next statement in the calling function is an if statement to determine what was returned, and act upon that:
$result = myfunc
Write-Host $result
if ($result -eq 'nowork') {
do this..
} elseif ($result -eq $false) {
do that..
} elseif ($result -eq $true) {
do something else..
}
And this is where it gets strange (to me). Even though I can see that True is returned, the if statement decides to go do 'do this..', the first branch of the if statement, where I would have expected that 'do something else..' would have been done.
Another strange thing is that it sometimes works, sometimes not. I tried changing the if statement to:
if ('nowork' -eq $result)
and then what went wrong first now worked, but later on the same issue re-appeared.
I'm guessing there's something wrong with my first string comparison, but I can't figure out what. I'm used to writing scripts in Linux (bash), so Powershell must be acting differently.
Btw: script is run in Debian 10, Powershell 7, but the exact same problem also appears on a Windows machine with Powershell 5.0.
Please help..
You're comparing apples and oranges
PowerShell's comparison operator behavior depends on type of the left-hand side operand.
When your lhs ($result) is a [bool] (ie. $true or $false), PowerShell will attempt to convert the right-hand side operand to [bool] as well before comparing the two.
Converting a non-empty string (ie. 'nowork') to [bool] results in $true, so the if condition evaluates to $true -eq $true -> $true.
You can fix this by manually type checking:
if($result -is [bool]){
if($result){
# was $true
}
else {
# was $false
}
}
elseif($result -eq 'nowork'){
# was 'nowork'
}
The nicer way of solving this however would be to always return the same type of object. In your case where you have 3 different return options, consider an enum:
enum WorkAmount
{
None
Some
All
}
function myfunc()
{
if (condition1) {
return [WorkAmount]::None
} elseif (condition2) {
return [WorkAmount]::Some
} elseif (condition3) {
return [WorkAmount]::All
}
}

PowerShell Switch Statement Issue

I have the following simple code and it isn't working (simplified from a much larger function)
The user in my first example doesn't exist in my use case
The switch statement doesn't work
A break-point (using ISE) on both the statements in the first switch never get triggered
The second example works without issue
The third code snippet is some troubleshooting code to prove $myADObject is null
What am I missing?
Snippet 1:
$user = "no.one"
$myADUsr = Get-ADObject -Filter { sAMAccountName -like $user }
switch ($myADUsr) {
$null { 'User object variable is null' }
default { 'User object variable has a value' }
}
Snippet 2:
$myADUsr = $null
switch ($myADUsr) {
$null { 'The variable is null' }
default { 'The variable has a value' }
}
Snippet 3:
clear-host
$member = "no.one"
$adobject = Get-ADObject -Filter { sAMAccountName -like $member }
'=== Frist switch ==='
switch ($adobject) {
{$null} { "tests as null"}
{$null -eq $_ } { 'another null test' }
{[string]::IsNullOrEmpty($_)} {'string null test'}
{$_ -eq [string]::Empty} { 'another string null test'}
{$null -ne $_ } { 'not null' }
default { "I don't think this is working ..." }
}
'==== if ====='
If ($null -eq $adobject) { 'null' } else { 'not null' }
'==== second switch ==='
$nullvariable = $null
switch ($nullvariable) {
$adobject { 'null object' }
$null { "null"}
default { "not null" }
}
The switch statement implicitly operates on collections (enumerable data types), and evaluates its branches for each element of the enumeration.
A function or cmdlet call that yields no output technically outputs the [System.Management.Automation.Internal.AutomationNull]::Value singleton, which can be conceived of as an array-valued $null - that is, in enumeration contexts such as switch it behaves like an empty collection: there's nothing to enumerate.
Therefore, because $myADUsr in your example contains [System.Management.Automation.Internal.AutomationNull]::Value due to Get-AdUser not producing any output, the switch statement is effectively skipped.
If all you need to know is whether an AD user object was returned, use PowerShell's implicit to-Boolean conversion in an if statement, because in an expression context [System.Management.Automation.Internal.AutomationNull]::Value behaves like $null (and therefore evaluates to $false):
$myADUsr = Get-ADObject -Filter 'sAMAccountName -like $user'
if ($myAdUsr) {
'User object variable has a value'
}
else {
'User object variable is null'
}
It think updating my original snippet #1 like this gets me out of trouble, it seems to work so I can continue to use the switch statement I have already written. I'm still testing.
$user = "no.one"
$myADUsr = Get-ADObject -Filter "sAMAccountName -like '$user'"
if ( #($myADUsr).Count -eq 0 ) { $myADUsr = $null }
switch ($myADUsr) {
$null { 'User object variable is null' }
default { 'User object variable has a value' }
}

Pester fails test even when exception is caught

I have a utility function that specifically handles an exception and ignores it, but when testing it with Pester the test fails, showing the exception that was already caught and handled. Am I missing something, or is this a bug in Pester?
This code reproduces the issue:
function Test-FSPath {
[cmdletbinding()]
param([string]$FileSystemPath)
if([string]::IsNullOrWhiteSpace($FileSystemPath)) { return $false }
$result = $false
try {
if(Test-Path $FileSystemPath) {
Write-Debug "Verifying that $FileSystemPath is a file system path"
$item = Get-Item $FileSystemPath -ErrorAction Ignore
$result = ($item -ne $null) -and $($item.PSProvider.Name -eq 'FileSystem')
}
} catch {
# Path pattern that Test-Path / Get-Item can't handle
Write-Debug "Ignoring exception $($_.Exception.Message)"
}
return ($result -or ([System.IO.Directory]::Exists($FileSystemPath)) -or ([System.IO.File]::Exists($FileSystemPath)))
}
Describe 'Test' {
Context Test-FSPath {
It 'returns true for a path not supported by PowerShell Test-Path' {
$absPath = "$env:TEMP\temp-file[weird-chars.txt"
[System.IO.File]::WriteAllText($absPath, 'Hello world')
$result = Test-FSPath $absPath -Debug
$result | Should -Be $true
Write-Host "`$result = $result"
Remove-Item $absPath
}
}
}
Expected result: Test passes
Actual result: Test fails:
[-] returns true for a path not supported by PowerShell Test-Path 2.62s
WildcardPatternException: The specified wildcard character pattern is not valid: temp-file[weird-chars.txt
ParameterBindingException: Cannot retrieve the dynamic parameters for the cmdlet. The specified wildcard character pattern is not valid: temp-file[weird-chars.txt
The exception you're seeing is not coming from your function, its coming from your use of Remove-Item which is throwing the error trying to remove the erroneous path (which also doesn't exist). You should just remove it as you never expect the item to be created anyway.
Or, alternatively (as mentioned in the comments) use TestDrive: which you then don't need to worry about cleaning up (seems for the path to be supported you need to use $Testdrive).
It 'returns true for a path not supported by PowerShell Test-Path' {
$absPath = "$env:TEMP\temp-file[weird-chars.txt"
[System.IO.File]::WriteAllText($absPath, 'Hello world')
$result = Test-FSPath $absPath
$result | Should -Be $true
}
As an aside, I generally tend to do execution type stuff outside of the It, and just test the result inside. When I started to do this for your code it showed me that the test was passing, as the error then moved to occurring in the Context block. Here's what I mean (also this example makes use of TestDrive: via the $testdrive variable):
Describe 'Test' {
Context Test-FSPath {
$absPath = "$testdrive\temp-file[weird-chars.txt"
[System.IO.File]::WriteAllText($absPath, 'Hello world')
$result = Test-FSPath $absPath
It 'returns true for a path not supported by PowerShell Test-Path' {
$result | Should -Be $true
}
}
}