Skip to content

assertEmpty() and assertNotEmpty() use overly restrictive phpstan-assert empty directives #6199

@lphilps

Description

@lphilps
Q A
PHPUnit version 11.5.18
PHP version 8.3.20
Installation Method Composer
Phpstan version 2.1.13
Laravel version 12.10.2

Summary

The phpunit asserts assertEmpty and assertNotEmpty are designed to work correctly on any object that implements the Countable trait and they do exactly that. isEmpty::matches checks to see if the passed parameter implements Countable and, if so, returns return count($other) === 0;.

However unit tests that take advantage of this behaviour/feature will fail phpstan's static analysis since the assertEmpty and assertNotEmpty methods include the phpstan directives

@phpstan-assert empty $actual

and

@phpstan-assert !empty $actual

respectively.

So unit tests that work correctly, fail phpstan

Current behavior

Unit tests behave correctly, but the code fails phpstan with

Call to method PHPUnit\Framework\Assert::assertEmpty() with                          
         Illuminate\Support\Collection<(int|string), mixed> will always evaluate to false.    
         🪪  method.impossibleType                                                            
         💡 Because the type is coming from a PHPDoc, you can turn off this check by setting

How to reproduce

This tiny sample unit test, AssertOnCollectionsTest, creates both empty and non-empty Laravel collections, which implement \Countable, and checks them with assertEmpty and assertNotEmpty.

<?php

namespace Tests;

use Illuminate\Support\Collection;

class AssertOnCollectionsTest extends \Illuminate\Foundation\Testing\TestCase
{
    // Should Pass
    public function testAssertEmptyOnEmpty()
    {
        $emptyCollection = collect();

        $this->assertEmpty($emptyCollection);
    }

    // Should Fail
    public function testAssertNotEmptyOnEmpty()
    {
        $emptyCollection = collect();

        $this->assertNotEmpty($emptyCollection);
    }

    // Should Fail
    public function testAssertEmptyOnNotEmpty()
    {
        $nonEmptyCollection = collect([1, 2, 3]);

        $this->assertEmpty($nonEmptyCollection);
    }

    // Should Pass
    public function testAssertNotEmptyOnNotEmpty()
    {
        $nonEmptyCollection = collect([1, 2, 3]);

        $this->assertNotEmpty($nonEmptyCollection);
    }
}

Running phpunit on this test results in the expected behaviour for the 4 tests: Pass, Fail, Fail, Pass

$ vendor/bin/phpunit tests/AssertOnCollectionsTest.php
PHPUnit 11.5.18 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.3.20

.FF.                                                                4 / 4 (100%)

Time: 00:00.642, Memory: 24.00 MB

There were 2 failures:

1) Tests\AssertOnCollectionsTest::testAssertNotEmptyOnEmpty
Failed asserting that an object is not empty.

/Users/lphilps/Sites/WhoPlusYou-Html5/tests/AssertOnCollectionsTest.php:22

2) Tests\AssertOnCollectionsTest::testAssertEmptyOnNotEmpty
Failed asserting that an object is empty.

/Users/lphilps/Sites/WhoPlusYou-Html5/tests/AssertOnCollectionsTest.php:30

FAILURES!
Tests: 4, Assertions: 4, Failures: 2.

That's good, but phpstan on the file produces errors:

$ vendor/bin/phpstan analyze tests/AssertOnCollectionsTest.php 
Note: Using configuration file .../phpstan.neon.dist.
 1/1 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

------ ---------------------------------------------------------------------------------------------------------------- 
  Line   AssertOnCollectionsTest.php                                                                                     
 ------ ---------------------------------------------------------------------------------------------------------------- 
  12     Call to method PHPUnit\Framework\Assert::assertEmpty() with Illuminate\Support\Collection<(int|string), mixed>  
         will always evaluate to false.                                                                                  
         🪪  method.impossibleType                                                                                       
         💡 Because the type is coming from a PHPDoc, you can turn off this check by setting                             
            treatPhpDocTypesAsCertain: false in your phpstan.neon.dist.                                                  
  20     Call to method PHPUnit\Framework\Assert::assertNotEmpty() with Illuminate\Support\Collection<(int|string),      
         mixed> will always evaluate to true.                                                                            
         🪪  method.alreadyNarrowedType                                                                                  
         💡 Because the type is coming from a PHPDoc, you can turn off this check by setting                             
            treatPhpDocTypesAsCertain: false in your phpstan.neon.dist.                                                  
  28     Call to method PHPUnit\Framework\Assert::assertEmpty() with Illuminate\Support\Collection<int, int> will        
         always evaluate to false.                                                                                       
         🪪  method.impossibleType                                                                                       
         💡 Because the type is coming from a PHPDoc, you can turn off this check by setting                             
            treatPhpDocTypesAsCertain: false in your phpstan.neon.dist.                                                  
  36     Call to method PHPUnit\Framework\Assert::assertNotEmpty() with Illuminate\Support\Collection<int, int> will     
         always evaluate to true.                                                                                        
         🪪  method.alreadyNarrowedType                                                                                  
         💡 Because the type is coming from a PHPDoc, you can turn off this check by setting                             
            treatPhpDocTypesAsCertain: false in your phpstan.neon.dist.                                                  
 ------ ---------------------------------------------------------------------------------------------------------------- 
                                                                                                    
 [ERROR] Found 4 errors                                                                             
                                                                                                    

Expected behavior

Phpstan should pass cleanly on this unit test.

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature/assertionIssues related to assertions and expectationstype/bugSomething is broken

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions