pester / Pester

Pester is the ubiquitous test and mock framework for PowerShell.
https://pester.dev/
Other
3.11k stars 473 forks source link

Should -Contain does not recognise my PSCustomObject insite my result collection #2574

Open KarstenHabay opened 3 hours ago

KarstenHabay commented 3 hours ago

Checklist

What is the issue?

I have a function that has two objects as input, which then returns the common properties with their values for comparison. Then, I wrote my first Pester test, and it doesn't see that the object in the collection is the same as the object contained in its collection as shown in the console output:

Expected @{Property=Name; CompareObject=John; WithObject=Jane} to be found in collection @(@{Property=Name; CompareObject=John; WithObject=Jane}, @{Property=Age; CompareObject=30; WithObject=30}), but it was not found.

Expected Behavior

It should find the object in the collection.

Steps To Reproduce

My PowerShell Function:

function Compare-ParlObjectsValue
{
    <#
        .SYNOPSIS
            Show common properties and their values of two objects for comparison.
        .DESCRIPTION
            This function returns an object with the common properties and their values of two objects, $CompareObject and $WithObject, for comparison. It excludes specified properties listed in $ExcludedProperty. If the 'ShowDifferent' switch is provided, it returns an object with only the properties where their values are different.
        .PARAMETER CompareObject
            The first object to compare.
        .PARAMETER WithObject
            The second object to compare.
        .PARAMETER ExcludedProperty
            An optional array of properties to exclude from comparison.
        .PARAMETER ShowDifferent
            A switch to indicate whether to return only properties with different values.
        .INPUTS
            System.Management.Automation.PSObject
                $CompareObject, $WithObject: Objects to compare
            System.String[]
                ExcludedProperty: Array of properties to exclude from result
            System.Management.Automation.SwitchParameter
                ShowDifferent: switch parameter, return only parameters with different values
        .OUTPUTS
            System.Management.Automation.PSCustomObject
                Custom object with common properties and their values of both objects, possibly limited by ExcludedProperty and/or ShowDifferent
        .EXAMPLE
            $object1 = [pscustomobject]@{ Name = "John"; Age = 30 }
            $object2 = [pscustomobject]@{ Name = "Jane"; Age = 30 }
            Compare-ParlObjectsValue -CompareObject $object1 -WithObject $object2
            This example compares the common properties of $object1 and $object2 and displays the properties and their values in a table.
        .EXAMPLE
            $object1, $object2 | Compare-ParlObjectsValue -ShowDifferent
            This example pipes two objects to Compare-ParlObjectsValue and displays only the results where the values differ between them.
        .EXAMPLE
            $object1, $object2 | Compare-ParlObjectsValue -ExcludedProperty "Age"
            This example excludes the "Age" property from the result between $object1 and $object2.
        .EXAMPLE
            $object1, $object2 | Compare-ParlObjectsValue -ShowDifferent -ExcludedProperty "Age"
            This example compares $object1 and $object2, excluding the "Age" property and displaying only the results with differing values.
        .NOTES
            Author: Karsten Habay
    #>

    [CmdletBinding()]
    [OutputType([System.Management.Automation.PSCustomObject])]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0
        )]
        [psobject]
        $CompareObject,

        [Parameter(
            Mandatory = $true,
            Position = 1
        )]
        [psobject]
        $WithObject,

        [Parameter(
            Position = 2
        )]
        [string[]]
        $ExcludedProperty,

        [Parameter()]
        [switch]
        $ShowDifferent
    ) #param

    begin {
        Write-Verbose "[BEGIN  ] Starting  : $($MyInvocation.Mycommand)"
        Write-Verbose "[META   ] Computer  : $env:COMPUTERNAME"
        Write-Verbose "[META   ] User      : $env:USERDOMAIN\$env:USERNAME"
        Write-Verbose "[META   ] Command   : $($MyInvocation.Mycommand)"
        Write-Verbose "[META   ] PSEdition : $($PSVersionTable.PSEdition)"
        Write-Verbose "[META   ] PSVersion : $($PSVersionTable.PSVersion)"
        Write-Verbose "[META   ] Test Date : $(Get-Date)"
        Write-Verbose "[BEGIN  ] PARAMETER : CompareObject     = $CompareObject"
        Write-Verbose "[BEGIN  ] PARAMETER : WithObject        = $WithObject"
        Write-Verbose "[BEGIN  ] PARAMETER : ExcludedProperty = $ExcludedProperty"
        Write-Verbose "[BEGIN  ] PARAMETER : ShowDifferent     = $ShowDifferent"
    } #begin

    process {
        Write-Verbose "[PROCESS] Starting Process -->"
        Write-Verbose "[PROCESS] Retrieving common properties"
        $CommonProperties = $CompareObject.psobject.Properties.Name | Where-Object { $WithObject.psobject.Properties.Name -contains $_ }
        Write-Verbose "[PROCESS] Common properties are: $CommonProperties"

        Write-Verbose "[PROCESS] Creating table with common properties and values of both objects"
        $Table = foreach ($PropertyName in $CommonProperties)
        {
            Write-Verbose "[PROCESS] Checks if a property name is not in a list of excluded parameters and whether the 'ShowDifferent' parameter is not set or the values of the property in the two objects are different"
            if ($PropertyName -notin $ExcludedProperty -and ($PSBoundParameters.ContainsKey('ShowDifferent') -eq $false -or $CompareObject.$PropertyName -ne $WithObject.$PropertyName))
            {
                [pscustomobject]@{
                    Property      = $PropertyName
                    CompareObject = $CompareObject.$PropertyName
                    WithObject    = $WithObject.$PropertyName
                }
            } #if
        } #foreach PropertyName
        Write-Output $Table
        Write-Verbose "[PROCESS] <-- Ending Process"
    } #process

    end {
        Write-Verbose "[END    ] Ending : $($MyInvocation.Mycommand)"
    } #end
} #function Compare-ParlObjectsValue

My Pester test:

BeforeAll {
    $Script:ModuleName = 'Parl.Utility'
    Import-Module -Name $Script:ModuleName -Force
}

AfterAll {
    # Unload the module being tested so that it doesn't impact any other tests.
    Get-Module -Name $Script:ModuleName -All | Remove-Module -Force
}

Describe 'Compare-ParlObjectsValue' {

    Context 'Happy Path' {

        It 'Should return common properties and their values' {
            # Arrange
            $object1 = [pscustomobject]@{ Name = "John"; Age = 30 }
            $object2 = [pscustomobject]@{ Name = "Jane"; Age = 30 }

            # Act
            $result = Compare-ParlObjectsValue -CompareObject $object1 -WithObject $object2

            # Assert
            $result | Should -BeOfType [pscustomobject]
            $resultObject1 = [pscustomobject]@{
                Property      = 'Name'
                CompareObject = 'John'
                WithObject    = 'Jane'
            }
            $resultObject2 = [pscustomobject]@{
                Property      = 'Age'
                CompareObject = 30
                WithObject    = 30
            }

            $result | Should -Contain $resultObject1
            $result | Should -Contain $resultObject2
        } #It

    } #Happy Path

}

Actual console output:

Starting discovery in 1 files.
Discovery found 1 tests in 17ms.
Running tests.
[-] Compare-ParlObjectsValue.Happy Path.Should return common properties and their values 7ms (6ms|1ms)
 Expected @{Property=Name; CompareObject=John; WithObject=Jane} to be found in collection @(@{Property=Name; CompareObject=John; WithObject=Jane}, @{Property=Age; CompareObject=30; WithObject=30}), but it was not found.
 at $result | Should -Contain $resultObject1, H:\PowerShell\Source\Parl.Utility\tests\Unit\Public\Compare-ParlObjectsValue.tests.ps1:36
 at <ScriptBlock>, H:\PowerShell\Source\Parl.Utility\tests\Unit\Public\Compare-ParlObjectsValue.tests.ps1:36
Tests completed in 189ms
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

Describe your environment

Windows 11 24H2 Visual Studio Code 1.95.1 PowerShell 7.4.6 Pester 5.6.1 Using Sampler 0.118.1

Possible Solution?

Debug Pester or Debug Me?

fflaten commented 2 hours ago

Should -Contain looks for the same object. Yours are different objects even though they have the same properties and values, so the behavior is expected.

What you need is something like Should-BeEquivalant available in Pester v6.0.0-alpha5 version which compares two different objects by properties. Combine it with Should-BeAny to find at least one match in the collection.

E.g. $result | Should-Any { $_ | Should-BeEquivalent $resultObject1 }

You can also use Assert-Any and Assert-Equivalent from the module Assert with Pester v5

KarstenHabay commented 1 hour ago

Tried this (after adding Module Assert v0.9.7):

$resultObject1 = [pscustomobject]@{
    Property      = 'Name'
    CompareObject = 'John'
    WithObject    = 'Jane'
}
$result | Assert-Any { $_ | Assert-Equivalent $resultObject1 }

but it didn't work.

Got message:

Starting discovery in 1 files.
Discovery found 1 tests in 18ms.
Running tests.
[-] Compare-ParlObjectsValue.Happy Path.Should return common properties and their values 20ms (19ms|1ms)
 AssertionException: Expected and actual are not equivalent!
 Expected:
 PSObject{
     CompareObject=John;
     Property=Name;
     WithObject=Jane
 }
 Actual:
 PSObject{
     CompareObject=30;
     Property=Age;
     WithObject=30
 }
 Summary:
 Expected property .Property with value 'Name' to be equivalent to the actual value, but got 'Age'.
 Expected property .CompareObject with value 'John' to be equivalent to the actual value, but got '30'.
 Expected property .WithObject with value 'Jane' to be equivalent to the actual value, but got '30'.
 at Assert-Equivalent, H:\PowerShell\Source\Parl.Utility\output\RequiredModules\Assert\0.9.7\src\Equivalence\Assert-Equivalent.ps1:676
 at <ScriptBlock>, H:\PowerShell\Source\Parl.Utility\tests\Unit\Public\Compare-ParlObjectsValue.tests.ps1:67
 at Assert-Any, H:\PowerShell\Source\Parl.Utility\output\RequiredModules\Assert\0.9.7\src\Collection\Assert-Any.ps1:12
 at <ScriptBlock>, H:\PowerShell\Source\Parl.Utility\tests\Unit\Public\Compare-ParlObjectsValue.tests.ps1:67
Tests completed in 228ms
Tests Passed: 0, Failed: 1, Skipped: 0, Inconclusive: 0, NotRun: 0

I think it's the result of comparing the assertion with the second object. Cause the second object in the collection is the 'Age' object.

Assert-Any is not doing its job 🤔

fflaten commented 1 hour ago

Yeah, maybe Assert never supported nested assertions. Sorry about that. Can double check tomorrow, unless @nohwnd answers first. 🙂

We're modifying the code slightly while merging it into Pester v6.

KarstenHabay commented 1 hour ago

I think it would be better to modify Should -Contain in such a way that it does what it is supposed to do, that is, see if an object is inside a collection even if it is not that same object, just like the PowerShell Comparison Operators: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comparison_operators?view=powershell-7.4#containment-operators Just a hint for @nohwnd 😉