nohwnd / Assert

A set of advanced assertions for Pester to simplify how you write tests.
MIT License
101 stars 12 forks source link

Assert-Equal when Expected is a collection #23

Closed DarkLite1 closed 6 years ago

DarkLite1 commented 7 years ago

I'm trying to figure out what I missed in the code below:

Find-Waldo.Tests.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"

Describe 'Get-Stuff' {
    it  'test' {
        $ExpectedResult = @(
            [PSCustomObject]@{
                ADObjectName = 'BEL ROL-STAFF-IT Demand Management'
                Permission   = 'L'
            }
            [PSCustomObject]@{
                ADObjectName = 'BEL ROL-STAFF-IT Service Desk'
                Permission   = 'L'
            }
        )

        Assert-Equal -Actual $TheStuff -Expected $ExpectedResult
    }
}

Find-Waldo.ps1

Function Get-Stuff {
    [PSCustomObject]@{
        ADObjectName = 'BEL ROL-STAFF-IT Demand Management'
        Permission   = 'L'
    }
    [PSCustomObject]@{
        ADObjectName = 'BEL ROL-STAFF-IT Service Desk'
        Permission   = 'L'
    }
}

$TheStuff = Get-Stuff

I can't seem to find the reason why it's not matching the expectation?

nohwnd commented 7 years ago

You are not doing anythig wrong, I think, this is just one of the undefined behaviors where I am not sure how to proceed. The Assert-Equal uses -eq operator internally and there is no way to disable it's behavior regarding arrays. There I few options how I could handle this:

I will probably choose the first option and provide a different set of collection assertion that will have a better defined behavior. Silently comparing by reference does not seem like a useful thing to do and it complicates how the assertion works. The third option again complicates how it works, and tries to overload the assertion with too much stuff.

You might try assert equivalent that walks arrays and compares whole objects, but be warned that it is experimental. Assert-Equivalent -Expected $ExpectedResult -Actual $TheStuff

DarkLite1 commented 7 years ago

Yes, using Assert-Equivalent does fix the problem. It's a bit confusing the names at times. but it does the job. Thank you very much, really appreciate your help.

nohwnd commented 7 years ago

No problem, if you have better names I am open to change. :) I am used to that naming from Fluent Assertions (of which some of the functionality is a "clone"), so it's easy for me to see what things do, but might not be for people unfamiliar with the tool.

DarkLite1 commented 7 years ago

I see why you reason like this. I'm a total newbie in Pester testing but not in PowerShell scripting. Maybe it could be an idea to work with a switch? Something like:

Assert-Equal vs Assert-Equql -Collection? The best solution would be to just use Assert-Equal and have it figure out if it concerns a collection or not and then use the appropriate code to handle that case.

The latter is what I would consider. It takes away the pain for the user to decide what is coming, being a collection or a single item.

nohwnd commented 6 years ago

@DarkLite1 wanted to respond to this a while ago but forgot.

The latter is what I would consider. It takes away the pain for the user to decide what is coming, being a collection or a single item.

This should not be problem. I would only restrict the type of input on the Expected side, and there you always know what you are expecting. I must use the -eq operator like this $Expected -eq $Actual, and that produces non-trivial behavior when collection is provided on the Expected side.

For that reason I would disable providing collections on the Expected side, to keep the behavior of Assert-Equal extremely straightforware and provide alternative assertions that will deal with collections. Such as Assert-Any, Assert-All -IgnoreOrder.

nohwnd commented 6 years ago

To restrict the Expected side I need a reliable way of saying what is a collection, and this seems like a reliable way to tell if something is considered a collection by PowerShell.

We had similar discussion here, but there we only needed to walk the tree, and terminate on single items, so there we are not actually telling if the incoming object is a collection or now.

@alx9r could you have a look on this please? I am not sure if I miss something.

function IsCollection ($o) {
    # check for value types and strings explicitly
    # because otherwise it does not work for decimal
    # so let's skip all values we definitely know
    # are not collections
    if ($o -is [ValueType] -or $o -is [string])
    {
        return $false
    }

    -not [object]::ReferenceEquals($o, $($o))
}

Describe "Tests if object is collection" {
    It "returns `$false for non collection types <value>" -TestCases @(
        @{ Value = $null }

        @{ Value = 'a' }
        @{ Value = 1 }
        @{ Value = 1D }
        @{ Value = 1.1 }

        @{ Value = 101 }
        @{ Value = 101L }
        @{ Value = 101D }
        @{ Value = 101.1 }

        @{ Value = 1MB }
        @{ Value = 1DMB }
        @{ Value = 1.1MB }
        @{ Value = @{} }
        @{ Value = @{ Name = 'Jakub' } }
        @{ Value = (ps -Name Idle) }
    ) {
        param($Value)
        IsCollection $Value | Should -Be $false
    }

    It "returns `$true for collection types" -TestCases @(
        @{ Value = @() }
        @{ Value = (ps -Name Idle, Powershell) }
        @{ Value = [System.Collections.Generic.List[int]]1 }
        @{ Value = [System.Collections.Generic.List[decimal]]2 }
    ) {
        param($Value)
        IsCollection $Value | Should -Be $true
    }
}
alx9r commented 6 years ago

@nohwnd It seems like the function you are calling "IsCollection" ought to be answering one or more of the following questions:

  1. Is this something that PowerShell unrolls when piped to a command?
  2. Is this something that PowerShell unrolls when output from one command and input to another?
  3. Is this something that PowerShell unrolls when returned from one command and input to another?
  4. Is this something that PowerShell unrolls and re-rolls into an array when it is output from a command and assigned to a variable?
  5. Is this something that PowerShell unrolls and re-rolls into an array when it is returned from a command and assigned to a variable?
  6. Is this something that PowerShell unrolls when on the LHS of an operator?

I once expected these to have a set of answers that could be inferred or memorized for all objects. But I've seen so many quirks I don't believe that to be the case anymore. I have had success answering (1) through (5) for a (small) subset of types for Out-Collection (tests). Note how elaborate the logic is for Out-Collection compared with the IsCollection implementation proposed above. This is because of the irregularity of PowerShell's unrolling behavior amongst even the most common .Net collections. There is also some irregularity across PowerShell versions.

It may be that the answer to (6) can be inferred from the answers to (1) through (5). If that is the case, performing tests similar to Out-Collection might work, but Out-Collection's strategy has already turned out to be brittle to types that haven't explicitly been tested.

IsCollection and Out-Collection each use types as a proxy to infer behavior. I think a better way to test for (1) through (5) might be to directly test the behavior using PowerShell itself. I made an attempt at this in IsUnrolledByPipeline.

I haven't found a robust way to directly test the unrolling behavior of the LHS of operators to implement IsUnrolledByOperator. I think you need IsUnrolledByOperator for Assert-Equal. Hopefully the answer to (6) is the same as (1) through (5); in that case IsUnrolledByPipeline is an adequate proxy for IsUnrolledByOperator.

nohwnd commented 6 years ago

Restricted by 3af0dd18eb451d9565e7e39fac00151ec4febc75