Show content of hashtable when Pester test case fails - powershell

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.

Related

Pester having problems with else statement

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'
}
}
}

Allow function to run in parent scope?

Pardon the title wording if it's a bit confusing. . .
I have a very simple script that just dot sources a .ps1 file but, since it runs inside a function, it doesn't get loaded into the parent scope which is my ultimate objective.
function Reload-ToolBox {
Param (
[Parameter(Mandatory=$false)]
[ValidateSet('TechToolBox',
'NetworkToolBox','All')]
[string[]]$Name = 'TechToolBox'
)
Begin
{
$ToolBoxes = #{
TechToolBox = "\\path\to\my\ps1.ps1"
NetworkToolBox = "\\path\to\my\ps2.ps1"
}
if ($($PSBoundParameters.Values) -contains 'All') {
$null = $ToolBoxes.Add('All',$($ToolBoxes.Values | Out-String -Stream))
}
$DotSource = {
foreach ($PS1Path in $ToolBoxes.$ToolBox)
{
. $PS1Path
}
}
}
Process
{
foreach ($ToolBox in $Name)
{
Switch -Regex ($ToolBoxes.Keys)
{
{$_ -match "^$ToolBox$"} { & $DotSource }
}
}
}
End { }
}
Question:
How would I be able to load the ps1 being called in the function, into the parent scope?
google no helped:(
In order for dot-sourcing performed inside your function to also take effect for the function's caller, you must dot-source the function call itself (. Reload-TooBox ...)
Unfortunately, there is no way to make this dot-sourcing automatic, but you can at least check whether the function was called via dot-sourcing, and report an error with instructions otherwise.
Here's a streamlined version of your function that includes this check:
function Reload-ToolBox {
[CmdletBinding()]
Param (
[ValidateSet('TechToolBox', 'NetworkToolBox', 'All')]
[string[]] $Name = 'TechToolBox'
)
Begin {
# Makes sure that *this* function is also being dot-sourced, as only
# then does dot-sourcing of scripts from inside it also take effect
# for the caller.
if ($MyInvocation.CommandOrigin -ne 'Internal') { # Not dot-sourced?
throw "You must DOT-SOURCE calls to this function: . $((Get-PSCallStack)[1].Position.Text)"
}
$ToolBoxes = #{
TechToolBox = "\\path\to\my\ps1.ps1"
NetworkToolBox = "\\path\to\my\ps2.ps1"
}
$ToolBoxes.All = #($ToolBoxes.Values)
if ($Name -Contains 'All') { $Name = 'All' }
}
Process {
foreach ($n in $Name) {
foreach ($script in $ToolBoxes.$n) {
Write-Verbose "Dot-sourcing $script..."
. $script
}
}
}
}

How to check if a PowerShell switch parameter is absent or false

I am building a PowerShell function that builds a hash table. I am looking for a way I can use a switch parameter to either be specified as absent, true or false. How can I determine this?
I can resolve this by using a [boolean] parameter, but I didn't find this an elegant solution. Alternatively I could also use two switch parameters.
function Invoke-API {
param(
[switch]$AddHash
)
$requestparams = #{'header'='yes'}
if ($AddHash) {
$requestparams.Code = $true
}
How would I get it to display false when false is specified and nothing when the switch parameter isn't specified?
To check whether a parameter was either passed in by the caller or not, inspect the $PSBoundParameters automatic variable:
if($PSBoundParameters.ContainsKey('AddHash')) {
# switch parameter was explicitly passed by the caller
# grab its value
$requestparams.Code = $AddHash.IsPresent
}
else {
# parameter was absent from the invocation, don't add it to the request
}
If you have multiple switch parameters that you want to pass through, iterate over the entries in $PSBoundParameters and test the type of each value:
param(
[switch]$AddHash,
[switch]$AddOtherStuff,
[switch]$Yolo
)
$requestParams = #{ header = 'value' }
$PSBoundParameters.GetEnumerator() |ForEach-Object {
$value = $_.Value
if($value -is [switch]){
$value = $value.IsPresent
}
$requestParams[$_.Key] = $value
}
You can use PSBoundParameter to check
PS C:\ > function test-switch {
param (
[switch]$there = $true
)
if ($PSBoundParameters.ContainsKey('there')) {
if ($there) {
'was passed in'
} else {
'set to false'
}
} else {
'Not passed in'
}
}
If you have a parameter that can be $true, $false or unspecified, then you might not want the [Switch] parameter type because it can only be $true or $false ($false is the same as unspecified). As an alternative, you can use a nullable boolean parameter. Example:
function Test-Boolean {
param(
[Nullable[Boolean]] $Test
)
if ( $Test -ne $null ) {
if ( $Test ) {
"You specified -Test `$true"
}
else {
"You specified -Test `$false"
}
}
else {
"You did not specify -Test"
}
}
even simpler:
function test{
[CmdletBinding()]
param(
[Parameter(Mandatory=$False,Position=1)][switch]$set
)
write-host "commence normal operation"
if(-not $set){"switch is not set, i execute this"}
else{"switch is set"}
}
output
enter image description here

Using a variables string value in variable name

It should work like:
$part = 'able'
$variable = 5
Write-Host $vari$($part)
And this should print "5", since that's the value of $variable.
I want to use this to call a method on several variables that have similar, but not identical names without using a switch-statement. It would be enough if I can call the variable using something similar to:
New-Variable -Name "something"
but for calling the variable, not setting it.
Editing to add a concrete example of what I'm doing:
Switch($SearchType) {
'User'{
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJUsers_ListBox.Items.Add($Item)
}
}
'Computer' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJComputers_ListBox.Items.Add($Item)
}
}
'Group' {
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJGroups_ListBox.Items.Add($Item)
}
}
}
I want this to look like:
ForEach($Item in $OBJResults_ListBox.SelectedItems) {
$OBJ$($SearchType)s_ListBox.Items.Add($Item)
}
You're looking for Get-Variable -ValueOnly:
Write-Host $(Get-Variable "vari$part" -ValueOnly)
Instead of calling Get-Variable every single time you need to resolve a ListBox reference, you could pre-propulate a hashtable based on the partial names and use that instead:
# Do this once, just before launching the GUI:
$ListBoxTable = #{}
Get-Variable OBJ*_ListBox|%{
$ListBoxTable[($_.Name -replace '^OBJ(.*)_ListBox$','$1')] = $_.Value
}
# Replace your switch with this
foreach($Item in $OBJResults_ListBox.SelectedItems) {
$ListBoxTable[$SearchType].Items.Add($Item)
}

How to differentiate not set parameter from $false, 0, empty string?

I have function that updates object in WMI. I want user to be able to specify in parameters only values that he wants to update. How can I do it?
function UpdateObject ([bool] $b1, [bool] $b2, [int] $n1, [string] $s1)
{
$myObject = GetObjectFromWmi #(...)
#(...)
#This is bad. As it overrides all the properties.
$myObject.b1 = $b1
$myObject.b2 = $b2
$myObject.n1 = $n1
$myObject.s1 = $s1
#This is what I was thinking but don't kwow how to do
if(IsSet($b1)) { $myObject.b1 = $b1 }
if(IsSet($b2)) { $myObject.b2 = $b2 }
if(IsSet($n1)) { $myObject.n1 = $n1 }
if(IsSet($s1)) { $myObject.s1 = $s1 }
#(...) Store myObject in WMI.
}
I tried passing $null as as parameter but it get's automaticly converted to $false for bool, 0 for int and empty string for string
What are your suggestions?
Check $PSBoundParameters to see if it contains a key with the name of your parameter:
if($PSBoundParameters.ContainsKey('b1')) { $myObject.b1 = $b1 }
if($PSBoundParameters.ContainsKey('b2')) { $myObject.b2 = $b2 }
if($PSBoundParameters.ContainsKey('n1')) { $myObject.n1 = $n1 }
if($PSBoundParameters.ContainsKey('s1')) { $myObject.s1 = $s1 }
$PSBoundParameters acts like a hashtable, where the keys are the parameter names, and the values are the parameters' values, but it only contains bound parameters, which means parameters that are explicitly passed. It does not contain parameters filled in with a default value (except for those passed with $PSDefaultParameterValues).
Building on briantist's answer, if you know that all the parameters exist as properties on the target object you can simply loop through the $PSBoundParameters hashtable and add them one by one:
foreach($ParameterName in $PSBoundParameters.Keys){
$myObject.$ParameterName = $PSBoundParameters[$ParameterName]
}
If only some of the input parameters are to be passed as property values, you can still specify the list just once, with:
$PropertyNames = 'b1','b2','n1','s1'
foreach($ParameterName in $PSBoundParameters.Keys |Where-Object {$PropertyNames -contains $_}){
$myObject.$ParameterName = $PSBoundParameters[$ParameterName]
}
To save yourself having to create a parameter for each property you may want to change, consider using a hashtable or other object to pass this information to your function.
For example:
function UpdateObject ([hashtable]$properties){
$myObject = GetObjectFromWmi
foreach($property in $properties.Keys){
# without checking
$myObject.$property = $properties.$property
# with checking (assuming members of the wmiobject have MemberType Property.
if($property -in (($myObject | Get-Member | Where-Object {$_.MemberType -eq "Property"}).Name)){
Write-Output "Updating $property to $($properties.$property)"
$myObject.$property = $properties.$property
}else{
Write-Output "Property $property not recognised"
}
}
}
UpdateObject -properties {"b1" = $true; "b2" = $false}
If you want a [Boolean] parameter that you want the user to specify explicitly or omit (rather than a [Switch] parameter which can be present or not), you can use [Nullable[Boolean]]. Example:
function Test-Boolean {
param(
[Nullable[Boolean]] $Test
)
if ( $Test -ne $null ) {
if ( $Test ) {
"You specified -Test `$true"
}
else {
"You specified -Test `$false"
}
}
else {
"You did not specify -Test"
}
}
In this sample function the $Test variable will be $null (user did not specify the parameter), $true (user specified -Test $true), or $false (user specified -Test $false). If user specifies -Test without a parameter argument, PowerShell will throw an error.
In other words: This gives you a tri-state [Boolean] parameter (missing, explicitly true, or explicitly false). [Switch] only gives you two states (present or explicitly true, and absent or explicitly false).