interitty / phpunit
扩展了sebastianbergmann/phpunit框架。
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: ~1.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
扩展了sebastianbergmann/phpunit框架。
要求
- PHP >= 8.3
安装
安装interitty/phpunit的最佳方式是使用Composer
composer require --dev interitty/phpunit
配置
该包使用了一个针对PHPStan的扩展来更好地预测通过反射获得的返回类型。如果已安装phpstan/extension-installer服务,则不需要进一步配置;否则,需要将以下配置添加到项目根目录中的phpstan.neon
文件中。
includes:
- ./vendor/interitty/phpunit/src/PHPStan/extension.neon
功能
而不是使用标准的PHPUnit\Framework\TestCase
类,存在一个新的Interitty\PhpUnit\BaseTestCase
类,它提供了一些其他功能。
附加断言
除了标准的断言之外,此扩展还提供了一些附加功能。
assertSameContent()
assertSameContent(iterable $expected, iterable $actual[, string $message])
如果可迭代的$actual
不包含与可迭代的$expected
相同的内 容,则通过$message
报告错误。
示例:使用assertArray()
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class SameContentTest extends BaseTestCase
{
public function testSuccess(): void
{
$content = [1, 2, 3, 4];
$yield = static function ($content) {
foreach ($content as $key => $value) {
yield $key => $value;
}
};
self::assertSameContent($content, $yield($content));
}
}
expectExceptionCallback()
expectExceptionCallback(Closure $callback)
有时,可能需要将抛出的异常对象用于进一步的测试。因此,存在一个expectExceptionCallback
扩展,允许您定义一个回调,其中异常作为参数可用。
示例:使用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])
Interitty\Utils
扩展带来了Interitty\Exceptions\Exceptions::extend
函数,该函数允许扩展任何异常以添加支持,例如存储额外的数据,这些数据也用于检索翻译后的描述。expectExceptionData
扩展允许方便地验证这些数据。
示例:使用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);
}
}
工厂
Interitty\PhpUnit\BaseTestCase
提供了一些有用的工厂。
createMockAbstract()
createMockAbstract(string $className[, array $methods, array $addMethods])
工厂返回由$className
定义的MockObject
,并允许可选地模拟现有的$methods
和不存在 的$addMethods
。
示例:使用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])
在虚拟文件系统存储中提供新的临时目录。
示例:使用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])
在虚拟文件系统存储中提供新的临时文件。
示例:使用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));
}
}
数据提供者
Interitty\PhpUnit\BaseTestCase
提供了一些标准数据提供者。
alreadyDefinedStringDataProvider()
为测试字符串定义值的标准集合。
每个步骤包含参数:string $assertedData, string $message
参数 | 描述 |
---|---|
$assertedData | 断言的字符串 |
$message | 期望抛出的异常消息 |
示例请参阅文档中的以下部分。
stringDataProvider()
测试字符串支持值的标准化值集。
每个步骤包含参数:string $assertedData
参数 | 描述 |
---|---|
$assertedData | 断言的字符串 |
使用示例请参考文档中的下方内容。
unsupportedStringDataProvider()
测试字符串不支持值的标准化值集。
每个步骤包含参数:mixed $assertedData, string $message
参数 | 描述 |
---|---|
$assertedData | 断言数据 |
$message | 期望抛出的异常消息 |
使用示例请参考文档中的下方内容。
辅助工具
Interitty\PhpUnit\BaseTestCase
提供了一些实用的辅助工具。
callNonPublicMethod()
callNonPublicMethod($object, string $methodName[, array $argument])
提供了在 $object
上调用非公开方法 ($methodName
) 并传递一些 $arguments
的能力。
示例: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)
提供了从 $object
中获取非公开属性 ($propertyName
) 值的能力。
示例:getNonPublicPropertyValue() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class GetNonPublicPropertyClass
{
/** @var bool */
protected $propertyOne = true;
/** @var bool */
private $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)
提供了在 $object
中设置非公开属性 ($propertyName
) 值的能力。
示例:setNonPublicPropertyValue() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class SetNonPublicPropertyClass
{
/** @var bool */
protected $propertyOne = true;
/** @var bool */
private $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()
有时根据构造的字符串动态生成类/特质/接口可能很有用。为此,有一个辅助工具可以将传入的代码注册到自动加载器中。
示例: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);
}
}
标准测试器
由于 getter 和 setter 格式的标准化,还可以为它们提供标准单元测试。
processTestGetBoolDefault()
processTestGetBoolDefault(string|string[]$className, string $propertyName, bool $default)
标准布尔 getter 默认值的测试器
示例:processTestGetBoolDefault() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class GetBoolDefaultClass
{
/** @var bool */
protected $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)
标准 getter 默认值的测试器
示例:processTestGetDefault() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class GetDefaultClass
{
/** @var string */
protected $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)
标准 getter/setter 实现的测试器
示例:processTestGetSet() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class GetSetClass
{
/** @var string */
protected $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)
标准布尔 getter/setter 实现的测试器
示例:processTestGetSetBool() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
class GetSetBoolClass
{
/** @var bool */
protected $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)
标准 getter 为缺失必填值的测试器
示例:processTestGetUndefined() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
use Interitty\Utils\Validators;
use function assert;
class GetUndefinedClass
{
/** @var string */
protected $foo;
/**
* Foo getter
*
* @return string
*/
protected function getFoo(): string
{
assert(Validators::check($this->foo, 'string', 'foo before get'));
return $this->foo;
}
}
class GetUndefinedTest extends BaseTestCase
{
public function testSuccess(): void
{
$this->processTestGetUndefined(GetUndefinedClass::class, 'foo', 'string');
}
}
processTestSetAlreadyDefined()
processTestSetAlreadyDefined(string|string[]$className, string $propertyName, $value, string $message)
标准 setter 为覆盖已定义值的测试器
示例:processTestSetAlreadyDefined() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
use Interitty\Utils\Validators;
use function assert;
class SetAlreadyDefinedClass
{
/** @var string */
protected $foo;
/**
* Foo setter
*
* @param string $foo
* @return static Provides fluent interface
*/
protected function setFoo($foo)
{
assert(Validators::check($this->foo, 'null', 'foo before set'));
$this->foo = $foo;
return $this;
}
}
class SetAlreadyDefinedTest extends BaseTestCase
{
/**
* @dataProvider alreadyDefinedStringDataProvider
*/
public function testSuccess(string $string, string $message): void
{
$this->processTestSetAlreadyDefined(SetAlreadyDefinedClass::class, 'foo', $string, $message);
}
}
processTestSetAlreadyDefinedObject()
processTestSetAlreadyDefinedObject(string|string[] string $className, string $propertyName, object $value)
标准对象 setter 为覆盖已定义值的测试器
示例:processTestSetAlreadyDefinedObject() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
use Interitty\Utils\Validators;
use function assert;
class SetAlreadyDefinedObjectClass
{
/** @var object */
protected $foo;
/**
* Foo setter
*
* @param SetAlreadyDefinedObjectClass $foo
* @return static Provides fluent interface
*/
protected function setFoo(SetAlreadyDefinedObjectClass $foo)
{
assert(Validators::check($this->foo, 'null', 'foo before set'));
$this->foo = $foo;
return $this;
}
}
class SetAlreadyDefinedObjectTest extends BaseTestCase
{
public function testSuccess(): void
{
$object = new SetAlreadyDefinedObjectClass();
$this->processTestSetAlreadyDefinedObject(SetAlreadyDefinedObjectClass::class, 'foo', $object);
}
}
processTestSetUnsupportedValue()
processTestSetUnsupportedValue(string|string[] $className, string $propertyName, mixed $value, string $message)
标准 setter 为插入不支持值的测试器
示例:processTestSetUnsupportedValue() 的使用
<?php
declare(strict_types=1);
namespace Interitty\PhpUnit;
use Interitty\Utils\Validators;
use function assert;
class SetUnsupportedValueClass
{
/** @var string */
protected $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;
}
}
class SetUnsupportedValueTest extends BaseTestCase
{
/**
* @dataProvider unsupportedStringDataProvider
*/
public function testSuccess(mixed $foo, string $message): void
{
$this->processTestSetUnsupportedValue(SetUnsupportedValueClass::class, 'foo', $foo, $message);
}
}
集成测试用例
Interitty\PhpUnit\BaseTestCase
类旨在用于“单元测试”,但有时需要与依赖注入容器一起工作。本包附带 Interitty\PhpUnit\BaseIntegrationTestCase
类,该类提供了一个 createContainer
工厂方法,有助于为每个测试需求生成特定的 DI 容器。
额外的集成工厂
Interitty\PhpUnit\BaseIntegrationTestCase
提供了一些有用的工厂。
createContainer()
createContainer(string $configFilePath)
DI 容器工厂方法。
示例: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
为了测试 SQL 数据库或使用 dibi/dibi 库,有一个 Interitty\PhpUnit\BaseDibiTestCase
类。
默认配置与内存中的 SQLite 兼容,也可以通过 setConfig()
方法进行更改。
数据库的结构和内容通过 setupDatabase
方法设置,如下例所示。
要访问 Dibi/Connection,只需使用同名的获取器 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>
}