eliashaeussler/phpunit-attributes

View on GitHub
README.md

Summary

Maintainability
Test Coverage
<div align="center">

# PHPUnit Attributes

[![Coverage](https://img.shields.io/coverallsCoverage/github/eliashaeussler/phpunit-attributes?logo=coveralls)](https://coveralls.io/github/eliashaeussler/phpunit-attributes)
[![Maintainability](https://img.shields.io/codeclimate/maintainability/eliashaeussler/phpunit-attributes?logo=codeclimate)](https://codeclimate.com/github/eliashaeussler/phpunit-attributes/maintainability)
[![CGL](https://img.shields.io/github/actions/workflow/status/eliashaeussler/phpunit-attributes/cgl.yaml?label=cgl&logo=github)](https://github.com/eliashaeussler/phpunit-attributes/actions/workflows/cgl.yaml)
[![Tests](https://img.shields.io/github/actions/workflow/status/eliashaeussler/phpunit-attributes/tests.yaml?label=tests&logo=github)](https://github.com/eliashaeussler/phpunit-attributes/actions/workflows/tests.yaml)
[![Supported PHP Versions](https://img.shields.io/packagist/dependency-v/eliashaeussler/phpunit-attributes/php?logo=php)](https://packagist.org/packages/eliashaeussler/phpunit-attributes)

</div>

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

## 🔥 Installation

[![Packagist](https://img.shields.io/packagist/v/eliashaeussler/phpunit-attributes?label=version&logo=packagist)](https://packagist.org/packages/eliashaeussler/phpunit-attributes)
[![Packagist Downloads](https://img.shields.io/packagist/dt/eliashaeussler/phpunit-attributes?color=brightgreen)](https://packagist.org/packages/eliashaeussler/phpunit-attributes)

```bash
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:

```diff
 <?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:

```diff
     <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]`](#requiresclass)
* [`#[RequiresPackage]`](#requirespackage)

### [`#[RequiresClass]`](src/Attribute/RequiresClass.php)

_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`):

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

#### Example

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

<details>
<summary>More examples</summary>

#### Require single class

Class level:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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.
    }
}
```

</details>

### [`#[RequiresPackage]`](src/Attribute/RequiresPackage.php)

_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:
>
> ```xml
> <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`):

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

#### Example

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

<details>
<summary>More examples</summary>

#### Require explicit Composer package

Class level:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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:

```php
#[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:

```php
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.
    }
}
```

</details>

## 🧑‍💻 Contributing

Please have a look at [`CONTRIBUTING.md`](CONTRIBUTING.md).

## ⭐ License

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