Skip to content

-HaveCount assertion operator does not work when using strong typing on $null #1000

Closed

Description

@bergmeister

1. Provide a general summary of the issue in the Title above

Pester 4.2.0 introduced a new -HasCount assertion operator. When migrating existing tests, by replacing $variable.Count | Should -Be $expectedCount to $variable | Should -HaveCount $expectedCount, I found edge cases where this does not work. I created Pester tests to demonstrate this behaviour in a simple case but later on upon request I can also give 2 more comples examples where -HasCount does not work. The idea is to first investigate/fix the first simple problem and only afterwards approach the more complex test cases.

Describe "Pesters -HaveCount operator should not fail on `$null object that has Count property" {
    It "when not using strong typing" {
        $aliases = Get-Alias | Select-Object -First 0
        $aliases.Count | Should -Be 0
        $aliases | Should -HaveCount 0
    }

    It "when LHS is casted to [System.Array]" {
        [System.Array] $aliases = Get-Alias | Select-Object -First 0
        $aliases.Count | Should -Be 0
        $aliases | Should -HaveCount 0
    }

    It "when RHS is casted to [System.Array]" {
        $aliases = [System.Array] (Get-Alias | Select-Object -First 0)
        $aliases.Count | Should -Be 0
        $aliases | Should -HaveCount 0
    }

    It "when LHS and RHS are casted to [System.Array]" {
        [System.Array] $aliases = [System.Array] (Get-Alias | Select-Object -First 0)
        $aliases.Count | Should -Be 0
        $aliases | Should -HaveCount 0
    }
}

2. Describe Your Environment

Pester version : 4.3.1
PowerShell version : 5.1.16299.248
PowerShell version : 6.0.1
PowerShell version : 6.1.0-preview.589
OS version : Microsoft Windows NT 10.0.16299.0

3. Expected Behavior

All 4 tests above should pass

4.Current Behavior

The first test passed but not the other 3. By debugging it, the objects seem to be of the same type ($null), therefore I cannot explain why Pester behaves differently. PowerShell has .Count property on $null by design.

  Describing Pesters -HaveCount operator should not fail on $null object that has Count property
    [+] when not using strong typing 387ms
    [-] when LHS is casted to [System.Array] 132ms
      Expected an empty collection, but got collection with size 1 @().
      11:         $aliases | Should -HaveCount 0
      at <ScriptBlock>, C:\Users\cberg\git\bla.tests.ps1: line 11
    [-] when RHS is casted to [System.Array] 105ms
      Expected an empty collection, but got collection with size 1 @().
      17:         $aliases | Should -HaveCount 0
      at <ScriptBlock>, C:\Users\cberg\git\bla.tests.ps1: line 17
    [-] when LHS and RHS are casted to [System.Array] 37ms
      Expected an empty collection, but got collection with size 1 @().
      23:         $aliases | Should -HaveCount 0
      at <ScriptBlock>, C:\Users\cberg\git\bla.tests.ps1: line 23
Tests completed in 662ms
Tests Passed: 1, Failed: 3, Skipped: 0, Pending: 0, Inconclusive: 0

5. Possible Solution

Document more clearly on what types the new HasCount assertion operator works but to me it rather looks like a Pester internal bug that needs to be resolved.

6. Context

As described in paragraph: migrating to new -HasCount operator, i.e. not a blocking issue.

Activity

  1. self-assigned this
    on Feb 27, 2018
  2. markekraus commented on Feb 27, 2018

    @markekraus

    similarly.. I would expect this to work on HashTable and Dictionaries to could the number of Items in the dictionary.

    Describe "HaveCount issues" {
        It "Should Work with Dctionary<TKey, TValue>" {
            $Dictionary = [System.Collections.Generic.Dictionary[String,String]]::new()
            $Dictionary | Should -HaveCount 0
        }
        It "Should Work with HashTable" {
            $HashTable = @{}
            $HashTable | Should -HaveCount 0
        }
    }

    Result:

    Describing HaveCount issues
      [-] Should Work with Dctionary<TKey, TValue> 114ms
        Expected an empty collection, but got collection with size 1 @(System.Collections.Generic.Dictionary`2[System.String,System.String]).
        4:         $Dictionary | Should -HaveCount 0
      [-] Should Work with HashTable 74ms
        Expected an empty collection, but got collection with size 1 @(System.Collections.Hashtable).
        8:         $HashTable | Should -HaveCount 0
    
  3. bergmeister commented on Feb 27, 2018

    @bergmeister
    ContributorAuthor

    Yes, I have seen similar issues with different .net collection types as well but decided to rather raise this simple one first.

  4. JimMoyle commented on Aug 7, 2018

    @JimMoyle

    As the count property on objects returns non intuitive results as described above, I'd change $ActualValue.Count to ($ActualValue | Measure-Object).count This should fix the issues above.

  5. JimMoyle commented on Aug 7, 2018

    @JimMoyle

    Although a different solution would need to be included for PoSh 2.0 as measure-object was introduced in 3.0

  6. added this to the Better Should milestone on Dec 16, 2018
  7. fflaten commented on May 24, 2021

    @fflaten
    Contributor

    The original issue is mostly a PowerShell-issue of iterating $null through pipeline. MS fixed the same issue with foreach ($a in $null) { # ran once pre-v3 }-loop in PSv3, but this one is still there:

    $three = [System.Array] (1..3 | Where-Object { $false })
    [System.Array] $four = [System.Array] (1..3 | Where-Object { $false })
    
    $three | ForEach-Object { "hello three" }
    $four | ForEach-Object { "hello four" }
    
    # Output:
    hello three
    hello four

    Now that PowerShell v2 supports is dropped, the suggested fix with Measure-Object can be considered as it will fix these edge-cases. Doesn't look like it affects performance that much:

    > $p = (dir)*100
    > $p.Length
    3600
    > (Measure-Command { 1..10 | % { $p.Count } }).TotalMilliseconds
    7,8107
    > (Measure-Command { 1..10 | % { ($p | Measure-Object).Count } }).TotalMilliseconds
    68,4193

    As for the other issues mentioned here:

  8. nohwnd commented on May 25, 2021

    @nohwnd
    Member

    10 times slower seems like a lot. But it is a start.

  9. fflaten commented on May 25, 2021

    @fflaten
    Contributor

    It was even worse in my Unix container tbh, but with real life scenarios (fewer calls? Smaller collections?) it might not affect the total that much.

    We can probably scratch this approach as I just noticed it's a breaking change. It doesn't just ignore these edge cases, it drops null completely. Measure-Object will break @(1,2,$null,4) | Should -HaveCount 4

    I think the biggest issue here is the lack of null in the formatted error-message (Update: This is fixed in 5.3.0-alpha2)

  10. modified the milestones: Better Should, 6.0.0 on Jun 23, 2026
  11. nohwnd commented on Jun 25, 2026

    @nohwnd
    Member

    This comes down to what [System.Array] does to an empty pipeline. [System.Array] (Get-Alias | Select-Object -First 0) doesn't give you $null or @(), it gives you @($null) - a one-element array holding $null:

    [System.Array]$x = Get-Alias | Select-Object -First 0    # $x stays $null  -> HaveCount 0 passes
    $x = [System.Array](Get-Alias | Select-Object -First 0)   # $x is @($null) -> HaveCount sees 1

    Should -HaveCount uses -SupportsArrayInput, so it collects the pipeline and sees one item ($null), hence size 1. The LHS-cast case passes because there the variable just stays $null.

    So the open question is a behavior one: should <a single $null> | Should -HaveCount 0 pass? We'd have to treat a lone piped $null as empty, which then makes $null | Should -HaveCount 1 fail - surprising the other way. I'd rather decide that explicitly before changing -HaveCount. What do you think it should do?

  12. added a commit that references this issue on Jun 27, 2026
    592f15e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

AssertionsFor issues related with assertions

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions