eliashaeussler/phpunit-attributes

Provides additional attributes for tests with PHPUnit

1.2.1 2024-09-08 15:04 UTC

README

PHPUnit Attributes

Coverage Maintainability CGL Tests Supported PHP Versions

A Composer library with additional attributes to enhance testing with PHPUnit.

🔥 Installation

Packagist Packagist Downloads

composer require --dev eliashaeussler/phpunit-attributes

⚡ Usage

The library ships with a ready-to-use PHPUnit extension. It must be registered in your PHPUnit configuration file:

 <?xml version="1.0" encoding="UTF-8"?>
 <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
          bootstrap="vendor/autoload.php"
 >
+    <extensions>
+        <bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension" />
+    </extensions>
     <testsuites>
         <testsuite name="unit">
             <directory>tests</directory>
         </testsuite>
     </testsuites>
     <source>
         <include>
             <directory>src</directory>
         </include>
     </source>
 </phpunit>

Some attributes can be configured with custom extension parameters. These must be added to the extension registration section like follows:

     <extensions>
-        <bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension" />
+        <bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension">
+            <parameter name="fancyParameterName" value="fancyParameterValue" />
+        </bootstrap>
     </extensions>

🎢 Attributes

The following attributes are shipped with this library:

#[RequiresClass]

Scope: Class & Method level

With this attribute, tests or test cases can be marked as to be only executed if a certain class exists. The given class must be loadable by the current class loader (which normally is Composer's default class loader).

Configuration

By default, test cases requiring non-existent classes are skipped. However, this behavior can be configured by using the handleMissingClasses extension parameter. If set to fail, test cases with missing classes will fail (defaults to skip):

<extensions>
    <bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension">
        <parameter name="handleMissingClasses" value="fail" />
    </bootstrap>
</extensions>

Example

final class DummyTest extends TestCase
{
    #[RequiresClass(AnImportantClass::class)]
    public function testDummyAction(): void
    {
        // ...
    }
}
More examples

Require single class

Class level:

#[RequiresClass(AnImportantClass::class)]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass is missing.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if AnImportantClass is missing.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresClass(AnImportantClass::class)]
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass is missing.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require single class and provide custom message

Class level:

#[RequiresClass(AnImportantClass::class, 'This test requires the `AnImportantClass` class.')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass is missing, along with custom message.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if AnImportantClass is missing, along with custom message.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresClass(AnImportantClass::class, 'This test requires the `AnImportantClass` class.')]
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass is missing, along with custom message.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require single class and define custom outcome behavior

Class level:

#[RequiresClass(AnImportantClass::class, outcomeBehavior: OutcomeBehavior::Fail)]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Fails if AnImportantClass is missing.
    }

    public function testOtherDummyAction(): void
    {
        // Fails if AnImportantClass is missing.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresClass(AnImportantClass::class, outcomeBehavior: OutcomeBehavior::Fail)]
    public function testDummyAction(): void
    {
        // Fails if AnImportantClass is missing.
    }

    public function testOtherDummyAction(): void
    {
        // Does not fail.
    }
}

Require multiple classes

Class level:

#[RequiresClass(AnImportantClass::class)]
#[RequiresClass(AnotherVeryImportantClass::class)]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass and/or AnotherVeryImportantClass are missing.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if AnImportantClass and/or AnotherVeryImportantClass are missing.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresClass(AnImportantClass::class)]
    #[RequiresClass(AnotherVeryImportantClass::class)]
    public function testDummyAction(): void
    {
        // Skipped if AnImportantClass and/or AnotherVeryImportantClass are missing.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

#[RequiresPackage]

Scope: Class & Method level

This attribute can be used to define specific package requirements for single tests as well as complete test classes. A required package is expected to be installed via Composer. You can optionally define a version constraint and a custom message.

Important

The attribute determines installed Composer packages from the build-time generated InstalledVersions class built by Composer. In order to properly read from this class , it's essential to include Composer's generated autoloader in your PHPUnit bootstrap script:

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
>
    <!-- ... -->
</phpunit>

You can also pass the script as command option: phpunit --bootstrap vendor/autoload.php

Configuration

By default, test cases with unsatisfied requirements are skipped. However, this behavior can be configured by using the handleUnsatisfiedPackageRequirements extension parameter. If set to fail, test cases with unsatisfied requirements will fail (defaults to skip):

<extensions>
    <bootstrap class="EliasHaeussler\PHPUnitAttributes\PHPUnitAttributesExtension">
        <parameter name="handleUnsatisfiedPackageRequirements" value="fail" />
    </bootstrap>
</extensions>

Example

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console')]
    public function testDummyAction(): void
    {
        // ...
    }
}
More examples

Require explicit Composer package

Class level:

#[RequiresPackage('symfony/console')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if symfony/console is not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if symfony/console is not installed.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console')]
    public function testDummyAction(): void
    {
        // Skipped if symfony/console is not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require any Composer package matching a given pattern

Class level:

#[RequiresPackage('symfony/*')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if no symfony/* packages are installed.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if no symfony/* packages are installed.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/*')]
    public function testDummyAction(): void
    {
        // Skipped if no symfony/* packages are installed.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require Composer package with given version constraint

Class level:

#[RequiresPackage('symfony/console', '>= 7')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if installed version of symfony/console is < 7.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if installed version of symfony/console is < 7.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console', '>= 7')]
    public function testDummyAction(): void
    {
        // Skipped if installed version of symfony/console is < 7.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require Composer package and provide custom message

Class level:

#[RequiresPackage('symfony/console', message: 'This test requires the Symfony Console.')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if symfony/console is not installed, along with custom message.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if symfony/console is not installed, along with custom message.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console', message: 'This test requires the Symfony Console.')]
    public function testDummyAction(): void
    {
        // Skipped if symfony/console is not installed, along with custom message.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

Require Composer package and define custom outcome behavior

Class level:

#[RequiresPackage('symfony/console', outcomeBehavior: OutcomeBehavior::Fail)]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Fails if symfony/console is not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Fails if symfony/console is not installed.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console', outcomeBehavior: OutcomeBehavior::Fail)]
    public function testDummyAction(): void
    {
        // Fails if symfony/console is not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Does not fail.
    }
}

Multiple requirements

Class level:

#[RequiresPackage('symfony/console')]
#[RequiresPackage('guzzlehttp/guzzle')]
final class DummyTest extends TestCase
{
    public function testDummyAction(): void
    {
        // Skipped if symfony/console and/or guzzlehttp/guzzle are not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Skipped if symfony/console and/or guzzlehttp/guzzle are not installed.
    }
}

Method level:

final class DummyTest extends TestCase
{
    #[RequiresPackage('symfony/console')]
    #[RequiresPackage('guzzlehttp/guzzle')]
    public function testDummyAction(): void
    {
        // Skipped if symfony/console and/or guzzlehttp/guzzle are not installed.
    }

    public function testOtherDummyAction(): void
    {
        // Not skipped.
    }
}

🧑‍💻 Contributing

Please have a look at CONTRIBUTING.md.

⭐ License

This project is licensed under GNU General Public License 3.0 (or later).