interitty / phpunit
Extended sebastianbergmann/phpunit framework.
Requires
- php: ~8.3
- dg/composer-cleaner: ~2.2
- interitty/nb-remote-phpunit: ~1.0
- interitty/utils: ~1.0
- mikey179/vfsstream: ~1.6
- phpunit/php-code-coverage: ~9.2
- phpunit/phpunit: ~9.6
- sempro/phpunit-pretty-print: ~1.4
Requires (Dev)
- dibi/dibi: ~5.0
- interitty/code-checker: ~1.0
- nette/application: ~3.2
- nette/bootstrap: ~3.2
- nette/caching: ~3.3
- phpstan/phpstan-dibi: ~2.0
Suggests
- dibi/dibi: Database Abstraction Library
- nette/application: Full-stack component-based MVC kernel
- nette/bootstrap: Dependency Injection generator to configure and bootstrap nette/application
- nette/caching: Library with easy-to-use API and many cache backends
README
Extended sebastianbergmann/phpunit framework.
Requirements
- PHP >= 8.3
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
Parameter | Description |
---|---|
$assertedData | Asserted 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
Parameter | Description |
---|---|
$assertedData | Asserted data |
$message | Expected 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>
}