rybakit/phpunit-extras

为PHPUnit提供自定义注解和期望。

支持包维护!
rybakit

v0.2.4 2022-07-14 16:44 UTC

This package is auto-updated.

Last update: 2024-08-30 01:28:55 UTC


README

Quality Assurance

此仓库包含使您能够轻松创建和集成自定义注解和期望到PHPUnit框架的功能。换句话说,使用此库,您的测试可能看起来像这样

https://raw.githubusercontent.com/rybakit/phpunit-extras/media/phpunit-extras-example.png

其中

  1. MySqlServer ^5.6|^8.0 是一个自定义要求
  2. @sql 是一个自定义注解
  3. %target_method% 是一个注解占位符
  4. 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的布尔值。默认情况下,您可以在表达式中引用以下 超全局变量cookieenvgetfilespostrequestserver

示例

/**
 * @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文件。