ryunosuke/phpunit-extension

PHPUnit 流畅接口和自定义断言

v4.2.1 2024-08-04 11:07 UTC

README

描述

此包添加了流畅接口并提供自定义断言。

  • 例如 that('xxx')->isEqual('xxx')
  • 例如 that(1)->isInt()->isBetween(1, 9)
  • 例如 that('qwe asd zxc')->stringStartsWith('qwe')->stringEndsWith('zxc')

安装

{
    "require-dev": {
        "ryunosuke/phpunit-extension": "dev-master"
    }
}

用法

实际类

简化图表

# e.g. bootstrap.php

/**
 * @template T
 * @param T $actual
 * @return \ryunosuke\PHPUnit\Actual|T
 */
function that($actual)
{
    return new \ryunosuke\PHPUnit\Actual($actual);
}

// example TestCase
class ActualTest extends \PHPUnit\Framework\TestCase
{
    function test_fluent()
    {
        # fluent interface
        // means: assertThat(5, logicalAnd(isType('int'), greaterThanOrEqual(1), lessThanOrEqual(9)));
        that(5)->isInt()->isBetween(1, 9);
    }

    function test_prefixEach()
    {
        # "each*" asserts per values (assert AND all values)
        // means: assertThat(1, greaterThan(0)); assertThat(2, greaterThan(0)); assertThat(3, greaterThan(0));
        that([1, 2, 3])->eachGreaterThan(0);
    }

    function test_suffixAnyAll()
    {
        # "*Any" asserts multiple arguments (assert OR all arguments)
        // means: assertThat('hello world', logicalOr(stringContains('hello'), stringContains('noexists')));
        that('hello world')->stringContainsAny(['hello', 'noexists']);
        // ignore case (other arguments are normal)
        that('hello world')->stringContainsAny(['HELLO', 'noexists'], true);

        # "*All" asserts multiple arguments (assert AND all arguments)
        // means: assertThat('hello world', logicalAnd(stringContains('hello'), stringContains('world')));
        that('hello world')->stringContainsAll(['hello', 'world']);
    }

    function test_var_use()
    {
        # "var" returns property of original object (non-public access is possible)
        $object = new \ArrayObject(['x' => 'X', 'y' => 'Y'], \ArrayObject::ARRAY_AS_PROPS);
        $property = that($object)->var('x');
        assertThat($property, equalTo('X'));

        # "use" returns method's closure of original object (non-public access is possible)
        $object = new \ArrayObject(['x' => 'X', 'y' => 'Y'], \ArrayObject::ARRAY_AS_PROPS);
        $method = that($object)->use('getArrayCopy');
        assertThat($method(), equalTo(['x' => 'X', 'y' => 'Y']));
    }

    function test_arrayAccess()
    {
        # array access returns array's value and actual
        $array = ['x' => ['y' => ['z' => [1, 2, 3]]]];
        // means: assertThat($array['x']['y']['z'], equalTo([1, 2, 3]));
        that($array)['x']['y']['z']->isEqual([1, 2, 3]);
    }

    function test_propertyAccess()
    {
        # property access returns property and actual (non-public access is possible)
        $object = (object) ['x' => 'X'];
        // means: assertThat($object->x, equalTo('X'));
        that($object)->x->isEqual('X');
    }

    function test_methodCall()
    {
        # method call returns original result and actual (non-public access is possible)
        $object = new \ArrayObject([1, 2, 3]);
        // means: assertThat($object->getArrayCopy(), equalTo([1, 2, 3]));
        that($object)->getArrayCopy()->isEqual([1, 2, 3]);

        # actual's method prefers to original method
        $object = new \ArrayObject([1, 2, 3]);
        // means: assertThat($object, countOf(3)); not: $object->count();
        that($object)->count(3);

        # "callable" returns original method's callable and actual
        that($object)->callable('count')->isCallable();
        // "callable"'s arguments mean method arguments
        that($object)->callable('setIteratorClass', \stdClass::class)->throws('derived from ArrayIterator');

        # "do" invokes original method and actual
        that($object)->do('count')->isEqual(3);

        # "__invoke" returns original::__invoke and actual
        $object = function ($a, $b) { return $a + $b; };
        // means: assertThat($object(1, 2), equalTo(3));
        that($object)(1, 2)->isEqual(3);
    }

    function test_methodCallWithBinding()
    {
        # method call by (...[]) returns method's callable of original object with binding (non-public access is possible)
        $closure = function ($arg) { echo $arg; };
        that($closure)->callable('__invoke', 'hoge')->outputEquals('hoge');
        that($closure)(...['hoge'])->outputEquals('hoge');
    }

    function test_try()
    {
        # "try" is not thrown method call and actual
        $object = new \ReflectionObject((object) ['x' => 'X']);
        // returns original result and actual if not thrown
        that($object)->try('getProperty', 'x')->isInstanceOf(\ReflectionProperty::class);
        // returns thrown exception and actual if thrown
        that($object)->try('getProperty', 'y')->isInstanceOf(\ReflectionException::class);
    }

    function test_list()
    {
        # "list" returns reference argument and actual
        // means: (fn (&$ref) => $ref = 123)($dummy); assertThat($dummy, equalTo(123));
        $dummy = null;
        that(fn (&$ref) => $ref = 123)($dummy)->list(0)->isEqual(123);
    }

    function test_return()
    {
        # "return" returns original value
        $object = new \stdClass();
        assertSame($object, that($object)->return());
    }

    function test_eval()
    {
        # "eval" asserts directly constraint (variadic arguments OR all arguments)
        // means: assertThat('x', equalTo('x'));
        that('x')->eval(equalTo('x'));
        // means: assertThat('x', logicalOr(equalTo('x'), equalTo('y'), equalTo('z')));
        that('x')->eval(equalTo('x'), equalTo('y'), equalTo('z'));
    }

    function test_as()
    {
        # "as" describes failure text
        // means: assertThat('x', equalTo('notX'), 'this is failure message');
        that('x')->as('this is failure message')->isEqual('notX');
    }

    function test_break()
    {
        # "break" mark breakable test (converting Failure to Warning)
        that('x')->break()->isEqual('notX');
        // ...continued this case
    }

    function test_and_exit()
    {
        # "and" returns latest actual
        $object = new \ArrayObject(['x' => 'abcX', 'y' => 'abcY'], \ArrayObject::ARRAY_AS_PROPS);
        // "and" can call as property also as below
        that($object)
            ->x->stringStartsWith('abc')->and->stringLengthEquals(4)->exit()
            ->y->stringStartsWith('abc')->and->stringLengthEquals(4)->exit()
            ->getArrayCopy()->count(2)->and->hasKey('x');

        # but no need to use them as below
        $that = that($object);
        $that->getArrayCopy()->count(2)->hasKey('x')->hasKey('y');
        $that->x->stringStartsWith('abc')->stringLengthEquals(4);
        $that->y->stringStartsWith('abc')->stringLengthEquals(4);
    }

    function test_declare()
    {
        # declare is replaced below at runtime
        // that(['x', 'y', 'z'])->declare();
        that(['x', 'y', 'z'])->is(['x', 'y', 'z']);
    }
}

实际类的返回值或参数可以透明地使用原始方法,如下所示。

class Example
{
    private int $privateField = 0;

    public function getPrivate()
    {
        return $this->privateField;
    }

    public function setPrivate(int $field)
    {
        $this->privateField = $field;
    }
}

class ExampleTest extends \PHPUnit\Framework\TestCase
{
    function test()
    {
        // test object
        $example = that(new Example());

        // directry private access
        $example->privateField = 3;
        $example->privateField->is(3);

        // $field is actual
        $field = $example->getPrivate();
        $field->is(3);

        // but, $field can use to arguments
        $example->setPrivate($field);
    }
}

自定义约束

内部

别名

\ryunosuke\PHPUnit\Actual::$constraintVariations 在其他约束中搜索变体。

// Disable. Built-in constraints are not called
\ryunosuke\PHPUnit\Actual::$constraintVariations['isSame'] = false;
// Alias. This ables to use: $actual->isSame('other')
\ryunosuke\PHPUnit\Actual::$constraintVariations['isSame'] = IsIdentical::class;
// Construct. This ables to use: $actual->isArray()
\ryunosuke\PHPUnit\Actual::$constraintVariations['isArray'] = [IsType::class => [IsType::TYPE_ARRAY]];
// Mix. This ables to use: $actual->isNullOrString()
\ryunosuke\PHPUnit\Actual::$constraintVariations['isNullOrString'] = [IsNull::class, IsType::class => [IsType::TYPE_STRING]];
// Instance. This ables to use: $actual->lineCount(5)
\ryunosuke\PHPUnit\Actual::$constraintVariations['lineCount'] = new class(/* argument is used as default */0) extends \PHPUnit\Framework\Constraint\Constraint {
    private $lineCount;

    public function __construct(int $lineCount)
    {
        $this->lineCount = $lineCount;
    }

    protected function matches($other): bool
    {
        return $this->lineCount === (preg_match_all("#\\R#", $other) + 1);
    }

    public function toString(): string
    {
        return 'is ' . $this->lineCount . ' lines';
    }
};
// Shorthand instance by closure. This is the same as above
\ryunosuke\PHPUnit\Actual::$constraintVariations['lineCount2'] = function ($other, int $lineCount, string $delimiter = "\\R") {
    return $lineCount === (preg_match_all("#$delimiter#", $other) + 1);
};

用户定义

\ryunosuke\PHPUnit\Actual::$constraintNamespaces 在约束命名空间中搜索。

// This ables to use: $actual->yourConstraint()
\ryunosuke\PHPUnit\Actual::$constraintNamespaces['your\\namespace'] = 'your/constraint/directory';
// Disable. chain case function call
\ryunosuke\PHPUnit\Actual::$functionNamespaces = [];

代码补全

实际类使用 \ryunosuke\PHPUnit\Annotation 特性。如果您在项目空间中声明此类,则启用自定义方法和代码补全。

// e.g. bootstrap.php
namespace ryunosuke\PHPUnit {
    /**
     * @method \ryunosuke\PHPUnit\Actual isHoge()
     */
    trait Annotation
    {
        function isFuga(): \ryunosuke\PHPUnit\Actual {
        {
            return $this->eval(new \PHPUnit\Framework\Constraint\IsEqual('fuga'));
        }
    }
}

这允许使用 $actual->isH(oge) 补全和 $actual->isF(uga) 方法。

或者调用 \ryunosuke\PHPUnit\Actual::generateAnnotation。此方法通过 $constraintVariations$constraintNamespaces 返回注释。

TestCaseTrait

此特性提供测试实用工具。

  • trapThrowable
    • 如果指定异常被抛出,则跳过测试。
  • restorer
    • 重置函数基础值。当未设置时,恢复前值。
  • finalize
    • 在测试结束时运行闭包。
  • rewriteProperty
    • 重写私有/受保护属性。当未设置时,恢复前值。
  • tryableCallable
    • 将私有/受保护方法封装成闭包。并将参数与默认值绑定。
  • getEnvOrSkip
    • 返回 getenv()。如果没有值,则跳过测试。
  • getConstOrSkip
    • 返回 constant()。如果没有定义,则跳过测试。
  • getClassMap
    • 根据 composer 返回所有类 => 文件数组。
  • getClassByDirectory
    • 按目录返回类名。
  • getClassByNamespace
    • 按命名空间返回类名。
  • emptyDirectory
    • 准备临时目录并清理内容。
  • backgroundTask
    • 异步运行闭包。
  • report
    • 向测试结果页脚报告消息。

自定义打印器

此包提供进度打印器。仅在失败时输出。在成功时不会输出。

<phpunit printerClass="\ryunosuke\PHPUnit\Printer\ProgressPrinter">
</phpunit>

自定义其他

# e.g. bootstrap.php
ryunosuke\PHPUnit\Replacer::insteadOf();

Exporter

此包提供自定义导出器。此导出器在以下方面进行了更改。

  • 扩展字符串的最大字符宽度
  • 将二进制字符串更改为引号字符串
  • 不插入标记换行符
  • 将对象标识符从哈希更改为 id

CodeCoverage

此包提供自定义 CodeCoverage。此 CodeCoverage 在以下方面进行了更改。

  • 支持 @codeCoverageIgnore 后缀注释
    • 例如 foo(); // @codeCoverageIgnore because php8.1 only

发布

版本控制是语义版本控制。

4.2.1

  • [fixbug] 在 php8.2 中修复了错误
  • [merge] 3.20.2

4.2.0

  • [feature] 添加了 tryableCallable
  • [change] 修复了存根生成

4.1.0

  • [change] 修复了 ProgressPrinter

4.0.0

  • [change] php>=8.0
  • [*change] 删除了过时的功能

3.20.2

  • [fixbug] 修复了声明转义

3.20.1

  • [fixbug] 修复了 Start/End 被忽略

3.20.0

  • [feature] 使用兼容的原始类

3.19.0

  • [feature] 添加了 finalize
  • [feature] 改进了 Traversable

3.18.0

  • [feature] 添加了 VALID_DOMAIN/VALID_HOSTNAME 到 IsValid

3.17.0

  • [feature] 添加了 getClassMap/getClassByDirectory/getClassByNamespace
  • [特性] 添加了 IsTypeOf 约束

3.16.0

  • [特性] 添加了 insteadof
  • [变更] 废弃了清除全局状态

3.15.0

  • [重构] 代码格式和修复检查
  • [特性] 为其添加了清除状态
  • [修复bug] 修复了约束和方法调用混合的问题
  • [修复bug] 将 getXXXOrSkip 改为静态

3.14.0

  • [特性] 添加了 TraversableComparator
  • [修复bug] 修复了 self/static 类型
  • [修复bug] 修复了多次标记文件

3.13.1

  • [修复bug] 修复了测试失败时子进程未终止的问题
  • [修复bug] 修复了在 Windows 上发现的单引号

3.13.0

  • [特性] 添加了报告后的功能
  • [特性] generateStub 支持glob模式

3.12.0

  • [变更] 在警告测试中抑制了警告
  • [特性] 添加了 backgroundTask
  • [修复bug] 修复了 mixin 不附加未生成的存根

3.11.0

  • [变更] 更改了 ProgressPrinter 格式并支持可中断测试
  • [特性] 添加了 trapThrowable
  • [特性] 添加了 breakable
  • [变更] 废弃了函数调用者
  • [重构] 修复了错误的命名空间

3.10.1

  • [变更] 改变了存根类的层次结构
  • [修复bug] 修复了 __set 不设置祖先私有字段
  • [修复bug] 修复了 generateStub 丢失了原始类型
  • [修复bug] 修复了 generateStub 忽略了公共成员

3.10.0

  • [特性] 改进了 generateStub
  • [特性] 添加了 MatchesCountEquals 约束
  • [特性] 添加了如果实际参数则解包原始值
  • [特性] 添加了禁用功能选项
  • [变更] 废弃了使用对象的 __toString 的静态调用
  • [修复bug] 修复了由于异常隐式传递而引起的异常
  • [修复bug] 修复了文件系统函数拒绝空字符串
  • [修复bug] 修复了 __set 私有字段
  • [修复bug] 修复了 "debug" 方法始终返回 null

3.9.0

  • [变更] 修复了打印器的奇异性
    • 提高了可移植性
    • 优先指定列
    • 启用详细模式
    • 在中断时打印结果

3.8.1

  • [特性] 标记风险不进行断言
  • [特性] 添加了 wasOutputed/wasErrored/inElapsedTime 方法

3.8.0

  • [特性] 添加了 restorer
  • [特性] 添加了 get(Env|Const)OrSkip
  • [变更] 修复了 ExpectationFailedException 消息过大
  • [修复bug] 修复了输出被吞没

3.7.1

  • [修复bug] 修复了损坏的依赖关系

3.7.0

  • [修复bug] 修复了重复的注解
  • [特性] 添加了 Is 约束(比 IsEqual 更宽松)
  • [特性] 添加了 ClosesTo 约束
  • [特性] 添加了 DatetimeEquals 约束
  • [特性] 在文件系统中支持 SplFileInfo
  • [变更] 将 as 方法更改为可变参数

3.6.0

  • [重构] 将私有字段名称更改为与存根生成不兼容
  • [特性] 实现了禁用内置约束
  • [特性] 添加了 TestCaseTrait 特性
  • [特性] 添加了 declare 方法
  • [特性] 添加了新方法
  • [特性] 添加了 isUndefined 变体
  • [特性] 添加了 EqualsPath 约束
  • [修复bug] 修复了存根生成中没有 $
  • [修复bug] 修复了由于频繁的不必要的函数调用而严格执行
  • [修复bug] 修复了在 __callStatic 中未调用原始方法

3.5.0

  • [特性] 添加了 htmlMatchesArray 支持样式属性
  • [修复bug] 修复了 "try" 语句捕获必要的异常
  • [变更] 实现了 __callStatic 省略

3.4.0

  • [重构] 修复了注解
  • [特性] 添加了 ...[] 语法
  • [特性] 添加了 stdout 到结果属性
  • [特性] 添加了 htmlMatchesArray 支持类和闭包
  • [特性] 添加了 OutputMatches 变体
  • [修复bug] 修复了测试目标出错时文件位置在测试代码上
  • [修复bug] 修复了进度混乱

3.3.0

  • [特性] ProgressPrinter 显示失败时的文件位置
  • [特性] htmlMatchesArray 使理解 A 失败时更容易

3.2.0

  • [特性] 添加了 bootstrap.php 用于模板
  • [特性] 打印实际值

3.1.0

  • [特性] 为断言统计添加了 final 方法
  • [特性] 在 OutputMatches 约束中添加了 raw 标志
  • [特性] 为无方法可调用的函数添加了 fn 方法
  • [重构] 建立了自描述类

3.0.1

  • [修复bug] 在开发和发布期间供应商目录不同
  • [修复bug] 可调用函数在非闭包/对象上抛出异常

3.0.0

  • [*变更] 查看日志

2.0.1

  • [功能] 支持 PHP8

2.0.0

  • [*变更] 查看日志

1.2.0

  • [功能] 添加 Annotester 类
  • [功能] 添加简写闭包别名
  • [功能] 添加 int, float ValidType
  • [功能] 添加约束别名 mangle 参数
  • [功能] 添加 "and" 属性/方法
  • [修复bug] 支持静态属性/方法
  • [修复bug] 支持 $compatibleVersion 的次要/修补版本

1.1.2

  • [功能] 添加 "InTime" 约束
  • [功能] 添加 "callable" 方法
  • [变更] 弃用 "catch" 和 "print" 方法

1.1.1

  • [修复bug] get/offsetGet 实现泄露
    • __get: 使用 stringToStructure
    • offsetGet: 访问原始偏移

1.1.0

  • [功能] 添加版本控制属性
  • [功能] 添加 "prefixIs", "suffixIs" 别名
  • [功能] 在 get/offsetGet 中支持 Regex 和 JSONPath 和 JMESPath
  • [功能] 实现 "__toString" 方法
  • [功能] 添加依赖其他约束
  • [功能] 添加 "FileSizeIs" 约束
  • [变更] 改变 "Not" 位置(例如 NotFileExists -> FileNotExists)
    • "notFileExists" 仍可使用,但将来将被删除
  • [变更] 将 "all*" 改名为 "each*"
    • "all*" 仍可使用,但将来将被删除
  • [修复bug] 规范化目录分隔符

1.0.0

  • 发布 1.0.0
  • [变更] 极大的变化
  • [功能] 添加 "function" 方法
  • [功能] 添加 "foreach" 方法
  • [功能] 支持 "Throws" 多个参数

0.2.0

  • [功能] 添加 "var" 方法
  • [功能] 添加 "use" 方法
  • [功能] 添加 "print" 方法
  • [功能] 添加 "return" 方法
  • [功能] 添加 "OutputMatches" 约束
  • [变更] 删除 "autoback" 方法
  • [变更] 重命名类/方法

0.1.0

  • [功能] 添加 "*All" 方法
  • [功能] 添加 "try" 方法
  • [功能] 添加 "message" 方法
  • [功能] 添加 "__invoke" 方法
  • [功能] 添加 "file*" 约束
  • [功能] 替换为原始 "logical*" 约束
  • [功能] 变体添加 "is" 别名
  • [功能] 变体支持匿名类
  • [修复bug] 变体忽略参数
  • [变更] __get/__call 可以访问非公共成员

0.0.0

  • 发布

许可

MIT