interitty/phpunit

Extended sebastianbergmann/phpunit framework.

v1.0.13 2024-12-08 09:44 UTC

This package is auto-updated.

Last update: 2024-12-08 08:45:53 UTC


README

Extended sebastianbergmann/phpunit framework.

Requirements

Installation

The best way to install interitty/phpunit is using Composer:

composer require --dev interitty/phpunit

Configuration

The package uses an extension for PHPStan to better predict return types obtained by reflection. If the phpstan/extension-installer service is installed, no further configuration is needed, otherwise it is necessary to add the following configuration to the phpstan.neon file in the project root folder.

includes:
    - ./vendor/interitty/phpunit/src/PHPStan/extension.neon

Features

Instead of using the standard PHPUnit\Framework\TestCase class, there is a new Interitty\PhpUnit\BaseTestCase class that provides some other features.

Additional assertions

In addition to standard assertions, this extension provides some extras.

assertSameContent()

assertSameContent(iterable $expected, iterable $actual[, string $message]) Reports an error identified by $message if iterbale $actual doesn't contains the same content as iterable $expected.

Example: Usage of assertArray()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Generator;

class SameContentTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $content = [1, 2, 3, 4];
        $yield = static function (array $content): Generator {
            foreach ($content as $key => $value) {
                yield $key => $value;
            }
        };
        self::assertSameContent($content, $yield($content));
    }
}

expectExceptionCallback()

expectExceptionCallback(Closure $callback) Sometimes it can be useful to have a thrown exception object available for further testing. For this reason, there is an expectExceptionCallback extension that allows you to define a callback in which an exception is available as a parameter.

Example: Usage of expectExceptionCallback()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Exceptions\Exceptions;
use LogicException;
use Throwable;

class ExceptionCallbackTest extends BaseTestCase
{
    public function testExtendTranslate(): void
    {
        $this->expectExceptionCallback(static function (Throwable $exception): void {
            self::assertSame('Message with key "foo"', (string) $exception);
        });

        throw Exceptions::extend(LogicException::class)
                ->setMessage('Message with key ":key"')
                ->addData('key', 'foo');
    }
}

expectExceptionData()

expectExceptionData([array $data]) The Interitty\Utils extension brings with it the Interitty\Exceptions\Exceptions::extend function, which allows extending any exception to add, among other things, support for storing additional data that is also used to retrieve the translated description. The expectExceptionData extension allows this data to be conveniently validated.

Example: Usage of expectExceptionData()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Exceptions\Exceptions;
use LogicException;

class ExceptionDataTest extends BaseTestCase
{
    public function testExtendData(): void
    {
        $data = ['key' => 'foo'];
        $this->expectExceptionData($data);

        throw Exceptions::extend(LogicException::class)
                ->setMessage('Message with key ":key"')
                ->setData($data);
    }
}

Factories

The Interitty\PhpUnit\BaseTestCase provides some useful factories.

createMockAbstract()

createMockAbstract(string $className[, array $methods, array $addMethods]) Factory that return MockObject of the class defined by $className and allows to optionally mock existing $methods and non-existing $addMethods.

Example: Usage of createMockAbstract()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

abstract class FooClass
{
    abstract public function abstractMethod(): bool;

    public function isAccessible(): bool
    {
        return true;
    }
}

class MockAbstractTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $class = $this->createMockAbstract(FooClass::class, ['abstractMethod']);
        $class->expects(self::once())->method('abstractMethod')->willReturn(true);
        self::assertTrue($class->isAccessible());
        self::assertTrue($class->abstractMethod());
    }
}

createTempDirectory()

createTempDirectory([string $directoryName]) Provides a new temporary directory in virtual file system storage.

Example: Usage of createTempDirectory()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use function basename;

class CreateTempDirectoryTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $directoryName = 'directoryName';
        $tempDirectory = $this->createTempDirectory($directoryName);
        self::assertFileExists($tempDirectory);
        self::assertIsWritable($tempDirectory);
        self::assertSame($directoryName, basename($tempDirectory));
    }
}

createTempFile()

createTempFile([string $content, string $fileName]) Provides new temporary file in virtual file system storage.

Example: Usage of createTempFile()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Nette\Utils\FileSystem;

use function basename;

class CreateTempFileTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $content = 'Example of the file content';
        $fileName = 'tempFileName';
        $tempFile = $this->createTempFile($content, $fileName);
        self::assertFileExists($tempFile);
        self::assertIsWritable($tempFile);
        self::assertSame($content, FileSystem::read($tempFile));
        self::assertSame($fileName, basename($tempFile));
    }
}

DataProviders

The Interitty\PhpUnit\BaseTestCase provides some standard Data Providers.

stringDataProvider()

A standard set of values for a testing string supported values.

Each step contain parameters: string $assertedData

ParameterDescription
$assertedDataAsserted string

An example of use is below in the documentation.

unsupportedStringDataProvider()

A standard set of values for a testing string unsupported values.

Each step contain parameters: mixed $assertedData, string $message

ParameterDescription
$assertedDataAsserted data
$messageExpected thrown exception message

An example of use is below in the documentation.

Helpers

The Interitty\PhpUnit\BaseTestCase provides some handy helpers.

callNonPublicMethod()

callNonPublicMethod($object, string $methodName[, array $argument]) Provides ability of calling non-public method ($methodName) on $object with some $arguments.

Example: Usage of callNonPublicMethod()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class CallNonPublicMethodClass
{
    protected function isAccessible(): bool
    {
        return true;
    }
}

class CallNonPublicMethodTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $class = new CallNonPublicMethodClass();
        $isAccessible = $this->callNonPublicMethod($class, 'isAccessible');
        self::assertTrue($isAccessible);
    }
}
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class CallNonPublicMethodWithArgumentClass
{
    private function processData(string $data): string
    {
        return $data;
    }
}

class CallNonPublicMethodWithArgumentTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $data = 'test';
        $class = new CallNonPublicMethodWithArgumentClass();
        $result = $this->callNonPublicMethod($class, 'processData', [$data]);
        self::assertSame($data, $result);
    }
}

getNonPublicPropertyValue()

getNonPublicPropertyValue($object, string $propertyName) Provides ability of getting non-public property ($propertyName) value from $object.

Example: Usage of getNonPublicPropertyValue()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetNonPublicPropertyClass
{
    /** @var bool */
    protected bool $propertyOne = true;

    /** @var bool */
    private bool $propertyTwo = true;
}

class GetNonPublicPropertyTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $someClass = new GetNonPublicPropertyClass();
        $propertyOne = $this->getNonPublicPropertyValue($someClass, 'propertyOne');
        $propertyTwo = $this->getNonPublicPropertyValue($someClass, 'propertyTwo');
        self::assertSame($propertyOne, $propertyTwo);
    }
}

setNonPublicPropertyValue()

setNonPublicPropertyValue($object, string $propertyName, mixed $value) Provides ability of setting non-public property ($propertyName) value into the $object.

Example: Usage of setNonPublicPropertyValue()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class SetNonPublicPropertyClass
{
    /** @var bool */
    protected bool $propertyOne = true;

    /** @var bool */
    private bool $propertyTwo = true;
}

class SetNonPublicPropertyTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $someClass = new SetNonPublicPropertyClass();
        $this->setNonPublicPropertyValue($someClass, 'propertyOne', false);
        $this->setNonPublicPropertyValue($someClass, 'propertyTwo', false);

        self::assertFalse($this->getNonPublicPropertyValue($someClass, 'propertyOne'));
        self::assertFalse($this->getNonPublicPropertyValue($someClass, 'propertyTwo'));
    }
}

processRegisterAutoload()

Sometimes it can be useful to generate a Class / Trait / Interface "on the fly" based on the constructed string. For this purpose, a helper is available that registers the passed code in the autoloader.

Example: Usage of processRegisterAutoload()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Strings;
use Nette\PhpGenerator\Helpers;

use function class_exists;

class GeneratedCodeAutoloadTest extends BaseTestCase
{
    /** All available class / interface / namespace name constants */
    protected const NAME_NAMESPACE = 'Vendor\\Namespace';
    protected const NAME_DUMMY_CLASS = self::NAME_NAMESPACE . '\\DummyClass';

    public function testSuccess(): void
    {
        $className = self::NAME_DUMMY_CLASS;

        self::assertFalse(class_exists($className));
        $this->generateDummyClass($className);
        self::assertTrue(class_exists($className));
    }

    /**
     * Dummy class generator
     *
     * @param string $className
     * @return void
     */
    protected function generateDummyClass(string $className): void
    {
        $classShortName = Helpers::extractShortName($className);
        $namespace = Strings::before($className, '\\' . $classShortName, -1);
        $code = '<?php
declare(strict_types=1);

' . ((string) $namespace === '' ? '' : 'namespace ' . $namespace . ';') . '

class ' . $classShortName . ' {
}
';
        $this->processRegisterAutoload($className, $code);
    }
}

Standard testers

Thanks to the standardization of the getters & setters format, there can be also standard unit tests for them.

processTestGetBoolDefault()

processTestGetBoolDefault(string|string[]$className, string $propertyName, bool $default) Tester of standard bool getter for default value

Example: Usage of processTestGetBoolDefault()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetBoolDefaultClass
{

    /** @var bool */
    protected bool $foo = false;

    /**
     * Foo getter
     *
     * @return bool
     */
    protected function isFoo(): bool
    {
        return $this->foo;
    }
}

class GetBoolDefaultTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetBoolDefault(GetBoolDefaultClass::class, 'foo', false);
    }
}

processTestGetDefault()

processTestGetDefault(string|string[] $className, string $propertyName, mixed $default) Tester of standard getter for default value

Example: Usage of processTestGetDefault()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetDefaultClass
{

    /** @var string */
    protected string $foo = '';

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        return $this->foo;
    }
}

class GetDefaultTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetDefault(GetDefaultClass::class, 'foo', '');
    }
}

processTestGetSet()

processTestGetSet(string|string[] $className, string $propertyName, mixed $value) Tester of standard getter/setter implementation

Example: Usage of processTestGetSet()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetSetClass
{
    /** @var string */
    protected string $foo = 'foo';

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        return $this->foo;
    }

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(string $foo)
    {
        $this->foo = $foo;
        return $this;
    }
}

class GetSetTest extends BaseTestCase
{
    /**
     * @dataProvider stringDataProvider
     */
    public function testSuccess(string $foo): void
    {
        $this->processTestGetSet(GetSetClass::class, 'foo', $foo);
    }
}

processTestGetSetBool()

processTestGetSetBool(string|string[] $className, string $propertyName, bool $value) Tester of standard bool getter/setter implementation

Example: Usage of processTestGetSetBool()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

class GetSetBoolClass
{
    /** @var bool */
    protected bool $foo = false;

    /**
     * Foo checker
     *
     * @return bool
     */
    protected function isFoo(): bool
    {
        return $this->foo;
    }

    /**
     * Foo setter
     *
     * @param bool $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(bool $foo)
    {
        $this->foo = $foo;
        return $this;
    }
}

class GetSetBoolTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetSetBool(GetSetBoolClass::class, 'foo', true);
    }
}

processTestGetUndefined()

processTestGetUndefined(string|string[] $className, string $propertyName, string $expectation) Tester of standard getter for missing mandatory value

Example: Usage of processTestGetUndefined()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class GetUndefinedClass
{
    /** @var string */
    protected string $foo;

    /**
     * Foo getter
     *
     * @return string
     */
    protected function getFoo(): string
    {
        assert(Validators::check(isset($this->foo), 'initialized', 'foo before get'));
        return $this->foo;
    }
}

class GetUndefinedTest extends BaseTestCase
{
    public function testSuccess(): void
    {
        $this->processTestGetUndefined(GetUndefinedClass::class, 'foo');
    }
}

processTestSetAlreadyDefined()

processTestSetAlreadyDefined(string|string[]$className, string $propertyName, $value, string $message) Tester of standard setter for overwriting already defined value

Example: Usage of processTestSetAlreadyDefined()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class SetAlreadyDefinedClass
{
    /** @var string */
    protected string $foo;

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo(string $foo)
    {
        assert(Validators::check(isset($this->foo), 'uninitialized', 'foo before set'));
        $this->foo = $foo;
        return $this;
    }
}

class SetAlreadyDefinedTest extends BaseTestCase
{
    /**
     * @dataProvider stringDataProvider
     */
    public function testSuccess(string $string): void
    {
        $this->processTestSetAlreadyDefined(SetAlreadyDefinedClass::class, 'foo', $string);
    }
}

processTestSetUnsupportedValue()

processTestSetUnsupportedValue(string|string[] $className, string $propertyName, mixed $value, string $message) Tester of standard setter for inserting unsupported value

Example: Usage of processTestSetUnsupportedValue()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Interitty\Utils\Validators;

use function assert;

class SetUnsupportedValueClass
{
    /** @var string */
    protected string $foo;

    /**
     * Foo setter
     *
     * @param string $foo
     * @return static Provides fluent interface
     */
    protected function setFoo($foo)
    {
        assert(Validators::check($foo, 'string', 'Foo'));
        $this->foo = $foo;
        return $this;
    }
}

/**
 * @phpstan-import-type AssertionExceptionData from BaseTestCase
 */
class SetUnsupportedValueTest extends BaseTestCase
{
    /**
     * @phpstan-param AssertionExceptionData $data
     * @dataProvider unsupportedStringDataProvider
     */
    public function testSuccess(mixed $foo, array $data): void
    {
        $this->processTestSetUnsupportedValue(SetUnsupportedValueClass::class, 'foo', $foo, $data);
    }
}

Integration TestCase

The Interitty\PhpUnit\BaseTestCase class is designed for "unit tests", but sometimes is necessary to work with Dependency Injection Container. This package comes with the Interitty\PhpUnit\BaseIntegrationTestCase class, which comes with the createContainer factory method, which helps to generate one specific DI container for each tests needs.

Additional integration factories

The Interitty\PhpUnit\BaseIntegrationTestCase provides some useful factories.

createContainer()

createContainer(string $configFilePath) DI container factory method.

Example: Usage of createContainer()
<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Nette\DI\MissingServiceException;

use function get_class;

class CreateContainerTest extends BaseIntegrationTestCase
{
    public function testSuccess(): void
    {
        $configContent = '
services:
    CreateContainerTest:
        class: Interitty\PhpUnit\CreateContainerTest
';
        $configFilePath = $this->createTempFile($configContent, 'config.neon');
        $container = $this->createContainer($configFilePath);

        self::assertInstanceOf(CreateContainerTest::class, $container->getByType(CreateContainerTest::class));

        $thrownException = null;
        try {
            $service = $container->getByType(CreateContainerTest::class);
            self::assertSame(CreateContainerTest::class, get_class($service));
        } catch (MissingServiceException $exception) {
            $thrownException = $exception;
        }
        self::assertNull($thrownException);
    }
}

BaseDibiTestCase

For the needs of testing the SQL database or working with the dibi/dibi library, there is a Interitty\PhpUnit\BaseDibiTestCase class.

The default configuration works with SQLite in memory but also can be changed via the setConfig() method.

The structure and content of the database are set up via the setupDatabase method like in the following example.

To access the Dibi/Connection simply use the so-named getter getConnection.

<?php

declare(strict_types=1);

namespace Interitty\PhpUnit;

use Dibi\Connection;
use Dibi\Type;

use function iterator_to_array;

class DibiConnectionTest extends BaseDibiTestCase
{
    /**
     * @inheritdoc
     */
    protected function setupDatabase(Connection $connection): void
    {
        parent::setupDatabase($connection);
        $connection->query('create table Person (id int PRIMARY KEY, name varchar NOT NULL, active bool DEFAULT 1)');
        $connection->insert('Person', ['id' => 1, 'name' => 'test 1', 'active' => true])->execute();
        $connection->insert('Person', ['id' => 2, 'name' => 'test 2', 'active' => true])->execute();
        $connection->insert('Person', ['id' => 3, 'name' => 'test 3', 'active' => false])->execute();
    }

    // <editor-fold defaultstate="collapsed" desc="Integration tests">
    /**
     * Tester of dibi connection implementation
     *
     * @return void
     */
    public function testDibiConnection(): void
    {
        $expectedData = [
            ['id' => 1, 'name' => 'test 1', 'active' => true],
            ['id' => 2, 'name' => 'test 2', 'active' => true],
            ['id' => 3, 'name' => 'test 3', 'active' => false],
        ];

        $query = $this->getConnection()->select('*')->from('Person');

        $data = $query
            ->setupResult('setRowFactory', static function (array $data): array {
                return $data;
            })
            ->setupResult('setType', 'active', Type::Bool)
            ->getIterator();

        self::assertSame($expectedData, iterator_to_array($data));
    }

    // </editor-fold>
}