hashtableequalitypowershell-corepester

Comparing hashtables in Pester


What's the correct way to compare hashtables in Pester tests? i.e. the following gives the error: InvalidResult: Expected System.Collections.Hashtable, but got System.Collections.Hashtable.

$expected = @{one=1;two=2}
$actual = @{one=1;two=2}
$actual | Should -Be $expected

If I first cast these HastTables to PSCustomObjects I get InvalidResult: Expected @{one=1; two=2}, but got @{one=1; two=2}.

$expected = [PSCustomObject]@{one=1;two=2}
$actual = [PSCustomObject]@{one=1;two=2}
$actual | Should -Be $expected

Similarly, if we compare the arrays (KeyCollection) of the keys we get InvalidResult: Expected @('one', 'two'), but got @('one', 'two').

$expected = @{one=1;two=2}
$actual = @{one=1;two=2}
$actual.Keys | Should -Be $expected.Keys

I can of course write my own Test-HashTablesAreEqual method to ensure they array of keys exactly match and to then check the value of each key; but then I either have to throw a custom exception or else just return a true/false value and use that in Pester; which doesn't give a meaningful fail message (i.e. I'd want Expected @{one=1;two=2}, but got @{one=1;two=4} or similar).

I can break things down into a set of simpler comparisons, which gives slighly better output, but here I still can't easily see which key is giving the wrong value if there's a difference:

([string[]]$actual.Keys) | Should -Be ([string[]]$expected.Keys)
foreach ($key in $actual.Keys){$actual[$key] | Should -Be $expected[$key]}

Side note: I can use the -Because parameter to add some context to the above; e.g. specifying that I'm comparing the hashtable's keys, or stating which key's value I'm comparing... Still not ideal, but that makes it more sensible.

Alternatively I can work around this a bit by converting to Json; but that feels like a hack; and whilst it's worked for my basic tests, I suspect there's no guarantee about the order in which the keys would appear (especially if the actual's keys have any hash collisions when the table's constructed causing their ordering to be less predictable).

($actual | ConvertTo-Json) | Should -Be ($expected | ConvertTo-Json)

Is there a "correct" way to do this comparison which gives a helpful fail message should the expected and actual values not match?


Update: Current Solution

This is a solution I knocked up as a wrapper around some of the ideas above... This custom function calls Pester commands directly, so we still get the expected Pester outputs, rather than relying on the function to return something for Pester to then process. The Assert module mentioned by Mark Wragg sounds like a better option if it's made to be compatible with PS Core; but in the meantime, if having to roll my own logic anyway (e.g. for a custom assertion) this felt like a quick win:

function Compare-HashtablesInPester {
  [CmdletBinding(DefaultParameterSetName='AsHashtable')]
  Param (
    [Parameter(Mandatory)]
    [AllowNull()]
    [Hashtable]$Expected
    ,
    [Parameter(Mandatory, ValueFromPipeline, ParameterSetName='AsHashtable')]
    [AllowNull()]
    [Hashtable]$Actual
    ,
    [Parameter(Mandatory, ParameterSetName='AsPSCustomObject')]
    [AllowNull()]
    [PSCustomObject]$ActualObject
    ,
    [Parameter(ParameterSetName='AsPSCustomObject')]
    [Int32]$Depth = 20 # default for ConvertTo-Json is 2, default for ConvertFrom-Json is 1024... 20 seems a reasonable default for the 2
  )
  if (($null -eq $Expected) -or ($Expected.Count -eq 0)) {
    if ($PSCmdlet.ParameterSetName -eq 'AsHashtable') {
      $Actual | Should -BeNullOrEmpty
    } else {
      $ActualObject | Should -BeNullOrEmpty
    }
  } else {
    if ($PSCmdlet.ParameterSetName -eq 'AsPSCustomObject') {
      $Actual = $ActualObject | ConvertTo-Json -Depth $Depth | ConvertFrom-Json -Depth $Depth -AsHashTable
    }
    $Actual | Should -HaveCount $Expected.Count
    [object[]]$actualKeys = $Actual.Keys | Sort-Object
    [object[]]$expectedKeys = $Expected.Keys | Sort-Object
    $actualKeys | Should -BeExactly $expectedKeys -Because 'these are the given hashtable''s keys'
    foreach ($key in $Actual.Keys) {
      $Actual[$key] | Should -BeExactly $Expected[$key] -Because "this is the value of the [$key] key in the given hashtable"
    }
  }
}

Solution

  • I don't think there's any simple way to do this. Converting to JSON isn't a terrible option, particularly if you can force the hashtable to be [ordered] first.

    Jakub Jareš who maintaines Pester, has a side module called Assert, that implements some more advanced/custom assertions for use in Pester. You can find that here:

    It has Assert-Equivalent that can be used to compare objects. For example:

    $expected = @{one=1;two=2}
    $actual = @{one=1;two=3}
    
    Assert-Equivalent -Actual $actual -Expected $expected
    

    returns:

    Expected and actual are not equivalent!
    Expected:
    @{one=1; two=2}
    Actual:
    @{one=1; two=3}
    Summary:
    Expected hashtable '@{one=1; two=2}', but got '@{one=1; two=3}'.
    Expected property .two with value '2' to be equivalent to the actual value, but got '3'.
    At C:\Users\wragg\OneDrive\Documents\WindowsPowerShell\Modules\Assert\0.9.5\src\Equivalence\Assert-Equivalent.ps1:674 char:9
    +         throw [Assertions.AssertionException]$message
    +         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo          : OperationStopped: (:) [], AssertionException
        + FullyQualifiedErrorId : Expected and actual are not equivalent!
    Expected:
    @{one=1; two=2}
    Actual:
    @{one=1; two=3}
    Summary:
    Expected hashtable '@{one=1; two=2}', but got '@{one=1; two=3}'.
    Expected property .two with value '2' to be equivalent to the actual value, but got '3'.
    

    The module hasn't been recently updated however. It seems to work fine for me in Windows PowerShell but not in PowerShell 7.4 where I get

    Unable to find type [Assertions.AssertionException].
    

    When the objects do not match.

    You might at least find it useful to look at his implementation if you wanted to roll your own:

    Pester supports custom assertions as described here: