rybakit / phpunit-extras
为PHPUnit提供自定义注解和期望。
Requires
- php: ^7.1|^8
- phpunit/phpunit: ^7.1|^8|^9
Requires (Dev)
- php: ^7.1.3|^8
- composer/semver: ^1.5
- friendsofphp/php-cs-fixer: ^2.18
- ocramius/package-versions: ^1.4
- symfony/expression-language: ^3.3|^4|^5
- vimeo/psalm: ^3.9|^4
Suggests
- composer/semver: For using version-related requirements
- ocramius/package-versions: For using the 'package' requirement
- symfony/expression-language: For using expression-based requirements and/or expectations
README
此仓库包含使您能够轻松创建和集成自定义注解和期望到PHPUnit框架的功能。换句话说,使用此库,您的测试可能看起来像这样
其中
MySqlServer ^5.6|^8.0
是一个自定义要求@sql
是一个自定义注解%target_method%
是一个注解占位符expectSelectStatementToBeExecutedOnce()
是一个自定义期望。
目录
安装
composer require --dev rybakit/phpunit-extras
此外,根据您将使用哪些功能,您可能需要安装以下包
要使用版本相关要求
composer require --dev composer/semver
要使用“包”要求
composer require --dev ocramius/package-versions
要使用基于表达式的需求/期望
composer require --dev symfony/expression-language
要一次性安装所有内容,请运行
composer require --dev rybakit/phpunit-extras \ composer/semver \ ocramius/package-versions \ symfony/expression-language
注解
PHPUnit支持多种注解,完整的列表可以在这里找到。使用此库,您可以通过以下选项之一轻松扩展此列表
继承自基测试用例类
use PHPUnitExtras\TestCase; final class MyTest extends TestCase { // ... }
使用特质
use PHPUnit\Framework\TestCase; use PHPUnitExtras\Annotation\Annotations; final class MyTest extends TestCase { use Annotations; protected function setUp() : void { $this->processAnnotations(static::class, $this->getName(false) ?? ''); } // ... }
注册扩展
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" > <!-- ... --> <extensions> <extension class="PHPUnitExtras\Annotation\AnnotationExtension" /> </extensions> </phpunit>
然后,您可以使用库提供的或您自己创建的注解。
处理器
注解处理器是一个实现注解行为的类。
该库目前仅包含“必需”处理器。对于灵感以及更多注解处理器示例,请查看tarantool/phpunit-extras 包。
要求
此处理器通过允许您添加自己的要求扩展了标准的PHPUnit @requires 注解。
需求
该库包含以下要求
条件
格式
@requires condition <condition>
其中 <condition>
是一个任意表达式,应评估为true的布尔值。默认情况下,您可以在表达式中引用以下 超全局变量:cookie
、env
、get
、files
、post
、request
和 server
。
示例
/** * @requires condition server.AWS_ACCESS_KEY_ID * @requires condition server.AWS_SECRET_ACCESS_KEY */ final class AwsS3AdapterTest extends TestCase { // ... }
您还可以在表达式中定义自己的变量
use PHPUnitExtras\Annotation\Requirement\ConditionRequirement; // ... $context = ['db' => $this->getDbConnection()]; $annotationProcessorBuilder->addRequirement(new ConditionRequirement($context));
常量
格式
@requires constant <constant-name>
其中 <constant-name>
是常量名称。
示例
/** * @requires constant Redis::SERIALIZER_MSGPACK */ public function testSerializeToMessagePack() : void { // ... }
包
格式
@requires package <package-name> [<version-constraint>]
其中 <package-name>
是必需包的名称,而 <version-constraint>
是类似于composer的版本约束。有关支持的约束格式详细信息,请参阅Composer 文档。
示例
/** * @requires package symfony/uid ^5.1 */ public function testUseUuidAsPrimaryKey() : void { // ... }
占位符
占位符允许您在注解中动态包含特定值。占位符是包围在符号 %
中的任何文本。注解可以有任意数量的占位符。如果占位符未知,则会抛出错误。
以下是一个默认可用的占位符列表
目标类
示例
namespace App\Tests; /** * @example %target_class% * @example %target_class_full% */ final class FoobarTest extends TestCase { // ... }
在上面的例子中,%target_class%
将被替换为FoobarTest
,而%target_class_full%
将被替换为App\Tests\FoobarTest
。
目标方法
示例
/** * @example %target_method% * @example %target_method_full% */ public function testFoobar() : void { // ... }
在上面的例子中,%target_method%
将被替换为Foobar
,而%target_method_full%
将被替换为testFoobar
。
临时目录
示例
/** * @log %tmp_dir%/%target_class%.%target_method%.log testing Foobar */ public function testFoobar() : void { // ... }
在上面的例子中,%tmp_dir%
将被替换为sys_get_temp_dir()函数的返回值。
创建自己的注解
以图中的@sql
注解为例。为此,创建一个名为SqlProcessor
的处理类。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Annotation\Processor\Processor; final class SqlProcessor implements Processor { private $conn; public function __construct(\PDO $conn) { $this->conn = $conn; } public function getName() : string { return 'sql'; } public function process(string $value) : void { $this->conn->exec($value); } }
就是这样。这个处理类所做的只是注册@sql
标签并调用PDO::exec()
,将标签之后的所有内容作为参数传递。换句话说,注解@sql TRUNCATE TABLE foo
等价于$this->conn->exec('TRUNCATE TABLE foo')
。
此外,仅为了举例,让我们创建一个占位符解析器,用特定测试方法或类的唯一表名替换%table_name%
。这将允许使用动态表名而不是硬编码的表名。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Annotation\PlaceholderResolver\PlaceholderResolver; use PHPUnitExtras\Annotation\Target; final class TableNameResolver implements PlaceholderResolver { public function getName() : string { return 'table_name'; } /** * Replaces all occurrences of "%table_name%" with * "table_<short-class-name>[_<short-method-name>]". */ public function resolve(string $value, Target $target) : string { $tableName = 'table_'.$target->getClassShortName(); if ($target->isOnMethod()) { $tableName .= '_'.$target->getMethodShortName(); } return strtr($value, ['%table_name%' => $tableName]); } }
剩下的只是注册我们新的注解。
namespace App\Tests; use App\Tests\PhpUnit\SqlProcessor; use App\Tests\PhpUnit\TableNameResolver; use PHPUnitExtras\Annotation\AnnotationProcessorBuilder; use PHPUnitExtras\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuilder { return parent::createAnnotationProcessorBuilder() ->addProcessor(new SqlProcessor($this->getConnection())) ->addPlaceholderResolver(new TableNameResolver()); } protected function getConnection() : \PDO { // TODO: Implement getConnection() method. } }
之后,所有继承自App\Tests\TestCase
的类都将能够使用@sql
标签。
如果您忘记从注册注解的基础类继承或如果注解名称有误,库将警告您有关未知注解。
如前所述,注册注解的另一种方式是通过PHPUnit扩展。与上面的例子一样,您需要重写createAnnotationProcessorBuilder()
方法,但现在为AnnotationExtension
类。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Annotation\AnnotationExtension as BaseAnnotationExtension; use PHPUnitExtras\Annotation\AnnotationProcessorBuilder; class AnnotationExtension extends BaseAnnotationExtension { private $dsn; private $conn; public function __construct($dsn = 'mysql:host=localhost;dbname=test') { $this->dsn = $dsn; } protected function createAnnotationProcessorBuilder() : AnnotationProcessorBuilder { return parent::createAnnotationProcessorBuilder() ->addProcessor(new SqlProcessor($this->getConnection())) ->addPlaceholderResolver(new TableNameResolver()); } protected function getConnection() : \PDO { return $this->conn ?? $this->conn = new \PDO($this->dsn); } }
之后,注册您的扩展。
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" > <!-- ... --> <extensions> <extension class="App\Tests\PhpUnit\AnnotationExtension" /> </extensions> </phpunit>
要更改默认连接设置,请将新的DSN值作为参数传递。
<extension class="App\Tests\PhpUnit\AnnotationExtension"> <arguments> <string>sqlite::memory:</string> </arguments> </extension>
有关配置扩展的更多信息,请参阅此链接。
期望
PHPUnit提供了一些方法来为测试代码设置期望。最常用的可能是expectException*和expectOutput*方法族。库提供了一种轻松创建自定义期望的方法。
使用示例
以创建一个验证测试代码下创建文件的期望为例。让我们称它为FileCreatedExpectation
。
namespace App\Tests\PhpUnit; use PHPUnit\Framework\Assert; use PHPUnitExtras\Expectation\Expectation; final class FileCreatedExpectation implements Expectation { private $filename; public function __construct(string $filename) { Assert::assertFileDoesNotExist($filename); $this->filename = $filename; } public function verify() : void { Assert::assertFileExists($this->filename); } }
现在,为了能够使用这个期望,从PHPUnitExtras\TestCase
继承您的测试用例类(推荐)或包含PHPUnitExtras\Expectation\Expectations
特质。
use PHPUnit\Framework\TestCase; use PHPUnitExtras\Expectation\Expectations; final class MyTest extends TestCase { use Expectations; protected function tearDown() : void { $this->verifyExpectations(); } // ... }
之后,按以下方式调用您的期望
public function testDumpPdfToFile() : void { $filename = sprintf('%s/foobar.pdf', sys_get_temp_dir()); $this->expect(new FileCreatedExpectation($filename)); $this->generator->dump($filename); }
为了方便起见,您可以将此语句放入单独的方法中,并将期望分组到一个特质中。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Expectation\Expectation; trait FileExpectations { public function expectFileToBeCreated(string $filename) : void { $this->expect(new FileCreatedExpectation($filename)); } // ... abstract protected function expect(Expectation $expectation) : void; }
高级示例
多亏了Symfony的ExpressionLanguage组件,您可以在不费太多周折的情况下创建具有更复杂验证规则的期望。
以实现图中的expectSelectStatementToBeExecutedOnce()
方法为例。为此,创建一个表达式上下文,该上下文将负责收集关于SELECT
语句调用的必要统计信息。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Expectation\ExpressionContext; final class SelectStatementCountContext implements ExpressionContext { private $conn; private $expression; private $initialValue; private $finalValue; private function __construct(\PDO $conn, string $expression) { $this->conn = $conn; $this->expression = $expression; $this->initialValue = $this->getValue(); } public static function exactly(\PDO $conn, int $count) : self { return new self($conn, "new_count === old_count + $count"); } public static function atLeast(\PDO $conn, int $count) : self { return new self($conn, "new_count >= old_count + $count"); } public static function atMost(\PDO $conn, int $count) : self { return new self($conn, "new_count <= old_count + $count"); } public function getExpression() : string { return $this->expression; } public function getValues() : array { if (null === $this->finalValue) { $this->finalValue = $this->getValue(); } return [ 'old_count' => $this->initialValue, 'new_count' => $this->finalValue, ]; } private function getValue() : int { $stmt = $this->conn->query("SHOW GLOBAL STATUS LIKE 'Com_select'"); $stmt->execute(); return (int) $stmt->fetchColumn(1); } }
现在创建一个特质,其中包含所有我们的语句期望。
namespace App\Tests\PhpUnit; use PHPUnitExtras\Expectation\Expectation; use PHPUnitExtras\Expectation\ExpressionExpectation; trait SelectStatementExpectations { public function expectSelectStatementToBeExecuted(int $count) : void { $context = SelectStatementCountContext::exactly($this->getConnection(), $count); $this->expect(new ExpressionExpectation($context)); } public function expectSelectStatementToBeExecutedOnce() : void { $this->expectSelectStatementToBeExecuted(1); } // ... abstract protected function expect(Expectation $expectation) : void; abstract protected function getConnection() : \PDO; }
最后,将此特质包含到您的测试用例类中。
use App\Tests\PhpUnit\SelectStatementExpectations; use PHPUnitExtras\TestCase; final class CacheableRepositoryTest extends TestCase { use SelectStatementExpectations; public function testFindByIdCachesResultSet() : void { $repository = $this->createRepository(); $this->expectSelectStatementToBeExecutedOnce(); $repository->findById(1); $repository->findById(1); } // ... protected function getConnection() : \PDO { // TODO: Implement getConnection() method. } }
为了获取灵感和更多期望的示例,请查看tarantool/phpunit-extras包。
测试
在运行测试之前,必须安装开发依赖项。
composer install
然后,运行所有测试
vendor/bin/phpunit vendor/bin/phpunit -c phpunit-extension.xml
许可证
该库在MIT许可下发布。有关详细信息,请参阅捆绑的LICENSE文件。