xaerodegreaz/phpspock

PhpSpock 是 Spock 测试框架的 PHP 实现

0.2.0 2020-11-19 08:43 UTC

This package is auto-updated.

Last update: 2024-09-19 16:38:51 UTC


README

备注

截至 2020 年,原始仓库 http://github.com/aleczhang/PhpSpock 似乎已不再维护。我已经进行了一些小的更新,以确保最新的 PHPUnit 命名空间正确地为适配器工作,并使数组即使在不违反 PHP 语法规则的情况下也可以进行存根化。

默认情况下,任何没有显式数量的模拟交互都将隐式评估为 (1.._)(至少一次)。在此更改之前,对于像 $a->getFoo() >> 123 这样的表达式不会进行存根化。

我还对一些测试进行了小的更新,以确保新的默认存根化表达式能够正确工作。

PhpSpock

PhpSpock 是 Spock 测试框架的 PHP 实现。测试的语法尽可能地复制了 PHP 语言的语法。PhpSpock 是一个独立的库,但设计成可以与其他测试框架(如 PhpUnit)配合使用。

有用的链接

实现的功能

  • Spock 语法
  • 测试中对 "use" 类导入的支持
  • PhpUnit 框架适配器
  • 参数化
  • 多个 when->then 块对
  • 断言中的自定义错误消息
  • 支持在调试器下运行
  • Spock 风格的模拟(交互)

变更日志

0.1.1

  • 多个错误修复

0.1.2

  • 简化了基数语法。现在你可以省略 "(" 和 ")",并使用 +1、-1、+0 替代 (_..4) 这样的结构

已知问题

@specDebug 的问题

描述:当你使用 @specDebug 生成调试代码时,控制台会抛出一些错误。当你删除此注释时,情况也是一样。

原因:PhpUnitAdapter 修改了标记测试方法抵抗的类的代码(以插入调试代码)。现在当规范解析器尝试使用反射获取此文件中某个其他测试的正文时,它会失败,因为反射不会反映文件更改。

解决方案:如果你需要添加/删除 @specDebug 注释,只需执行两次 phpunit 命令,忽略所有出现的错误。调试代码仍然有效,并且应该在第二次运行时正确运行。

计划

要实现的功能

  • 使 assertionFailure 输出更具描述性
  • 创建适当的文档,并在 GitHub 页面上添加索引

许可证

许可证的全文作为 COPYING 和 COPYING.LESSER 文件附件。

PhpSpock is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

PhpSpock is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with PhpSpock.  If not, see <https://gnu.ac.cn/licenses/>.

版权所有 2011 Aleksandr Rudakov ribozz@gmail.com

安装

目前安装 PhpSpock 的唯一方法是检出 git 代码并将其适配到某个 PSR0 兼容的自动加载器。

你可以从 GitHub 检出源代码:git://github.com/ribozz/PhpSpock.git

当前稳定版本是:0.1

所以,像这样的

git clone git://github.com/ribozz/PhpSpock.git git checkout 0.1

与框架的集成

目前,仅支持 PhpUnit 框架。

PhpUnit 集成

PhpUnit 集成是可选的。它唯一提供的是,你不再需要在每个使用规范测试的测试用例中覆盖 runTest() 方法。

只需在你的通用 TestCase 或直接在 phpUnit 测试中覆盖 runTest 方法

<?php

    protected function runTest()
    {
        if (\PhpSpock\Adapter\PhpUnitAdapter::isSpecification($this)) {
            return \PhpSpock\Adapter\PhpUnitAdapter::runTest($this);
        } else {
            return parent::runTest();
        }
    }

实现自己的测试框架适配器

您可以查看 PhpUnitAdapter 并了解它是如何集成到 PhpSpock 类中的。PhpSpock 设计得易于集成到第三方库中。如果您遇到需要一些额外功能(事件、一些额外的接口方法等)的情况,请随时在 GitHub 上分叉仓库,并向主分支提交合并请求。但请记住,PhpSpock 核心应保持对任何类型的测试框架的无知,并使用事件系统与它们交互。

用户指南

简介

如您所知,PhpSpock 是 Spock 测试框架的克隆,因此您还可以阅读 SpockBasics 文档来了解两个框架底层理念的更多内容:[http://code.google.com/p/spock/wiki/SpockBasics]。

术语

"Spock 允许您编写描述感兴趣的系统(属性、方面)预期特征的规范。感兴趣的系统可以是单个类到整个应用程序之间的任何东西,也称为待规范系统(SUS)。特征的描述从待规范系统的特定快照及其协作者开始;这个快照称为特征的固定点。"(版权所有 SpockBasics)。

在本教程中,我将术语“规范”应用于特征方法。因为特征方法实际上是对特征的规范。这个假设与 Speck 框架的术语不同。

编写规范

准备工作

您可以为任何 PhpUnit 测试用例(或其他具有适当适配器的框架)添加规范味道。您所需的所有操作是在您的测试用例中重写 runTest() 方法。

<?php

    namespace MyExamples;

    use \PhpSpock\Adapter\PhpUnitAdapter as PhpSpock;

    class WithoutIntegrationTest extends \PHPUnit_Framework_TestCase
    {
        protected function runTest()
        {
            if (\PhpSpock\Adapter\PhpUnitAdapter::isSpecification($this)) {
                return \PhpSpock\Adapter\PhpUnitAdapter::runTest($this);
            } else {
                return parent::runTest();
            }
        }

        ...
    }

这种方法需要您在每个创建的测试类中重写 runTest() 方法,但允许您不依赖于扩展某个特定的 TestCase 实现。

另一种方法是将此方法放入您的公共测试用例中。

将测试用例方法转换为规范

准备工作完成后,您可以编写您的第一个规范。

要作为规范运行测试,您应该使用注解 @spec 标记它,或者给它一个以 "Spec" 结尾的名称。

<?php

    /**
     * @test
     * @spec
     */
    public function thisIsMySpecificationStyleTest()
    {
        ...
    }

    /**
     * @test
     */
    public function thisIsAlsoBecauseItEndsWithSpec()
    {
        ...
    }

注意!@spec 注解不是 @test 的替代品,因此您仍然应该在测试用例中添加 @test 注解或以 "test" 前缀开始方法名称。

规范语法

规范是有效的 PHP 代码,因此您的 IDE 不会对不良语法提出异议,甚至还会为您在规范中编写的所有代码提供良好的自动完成。

规范由块组成

<?php

    /**
     * @spec
     * @test
     */
    public function myTest()
    {
        setup:
        ...

        when:
        ...

        then:
        ...

        where:
        ...

        cleanup:
        ...
    }

每个块从块标签(块的名称后跟冒号)开始,后面跟着任意数量的代码行。

注意!您不能在规范代码中使用标签和 goto 运算符。否则,规范解析器会抱怨您的不良语法。

唯一必需的块是 "then",它还有别名 "expect"。

因此,最简单的规范看起来像

<?php

    /**
     * @spec
     * @test
     */
    public function makeSureIAmStillGoodInMathematics()
    {
        then:
        2 + 2 == 4;
    }

或者甚至更好

<?php

    /**
     * @spec
     * @test
     */
    public function makeSureIAmStillGoodInMathematicsWithExcept()
    {
        expect:
        2 + 2 == 4;
    }

"then" 块

Then 块是一组表达式,这些表达式可以是代码片段或断言。表达式由分号字符分隔。

断言是返回布尔值的代码片段。

注意!断言应返回确切的布尔结果才能成为断言。

示例

<?php

    /**
     * @spec
     * @test
     */
    public function assertionExamples()
    {
        expect:
        2 + 2 == 4;      // assertion - true, ignoring
        3 - 3;           // not an assertion - ignoring
        true;            // assertion - true, ignoring
        (bool) (2-2);    // assertion - expression is converted to boolean false, throwing an assertion exception
    }

断言中一个有趣的事情是,位于断言同一字符串上的注释将被添加到异常消息中。示例中最后一个断言的输出将是

There was 1 failure:

1) DocExamples\SpecificationSyntaxTest::assertionExamples
Expression (bool) (2-2) is evaluated to false.

assertion - expression is converted to boolean false, throwing an assertion exception

"when" 块

尽管您可以在 "then" 块中写入不仅限于断言的普通代码(让我们称之为 "动作"),但动作的更好位置是 "when" 块。

"Then" 块通常与 "when" 块成对工作。当块包含动作,而 "then" 块包含预期结果的断言

<?php

    /**
     * @spec
     * @test
     */
    public function whenThenExample()
    {
        when:
        $a = 1 + 2;

        then:
        $a == 3;
    }

这种块组合称为 "when-then" 对。甚至可以使用多个 "when-then" 对

<?php

    /**
     * @spec
     * @test
     */
    public function whenThenExampleWithSeveralPairs()
    {
        when:
        $a = 1 + 2;

        then:
        $a == 3;

        when_:
        $a += 4;

        then_:
        $a == 7;
    }

但需要考虑 PHP 语法限制:PHP 不允许一个类方法中有相同名称的多个标签,因此我们需要在块名称末尾添加下划线 "_"。您可以在块名称末尾添加尽可能多的下划线。下划线将被忽略。

更多对

<?php

    /**
     * @spec
     * @test
     */
    public function whenThenExampleWithMorePairs()
    {
        when:
        $a = 1 + 2;

        then:
        $a == 3;

        when_:
        $a += 4;

        then_:
        $a == 7;

        when__:
        $a -= 2;

        then__:
        $a == 5;
    }

"setup" 和 "cleanup" 块

"setup" 块是一个应包含测试初始化代码的块

<?php

    /**
     * @spec
     * @test
     */
    public function setupBlock()
    {
        setup:
        $a = 3 + rand(2, 4);

        expect:
        $a > 3;
    }

如果您愿意,也可以省略 "setup" 块标签

<?php

    /**
     * @spec
     * @test
     */
    public function setupBlockWithoutLabel()
    {
        $a = 3 + rand(2, 4);

        expect:
        $a > 3;
    }

在这种情况下,解析器将假定从方法开始到第一个标记块之前的所有代码都是 "setup" 块。

"cleanup" 块在测试完成后执行

<?php

    /**
     * @spec
     * @test
     */
    public function setupBlockWithCleanup()
    {
        setup:
        $temp = tmpfile();

        when:
        fwrite($temp, "writing to tempfile");

        then:
        notThrown('Exception');

        when_:
        fseek($temp, 0);
        $data = fread($temp, 1024);

        then_:
        $data == "writing to tempfile";

        cleanup:
        fclose($temp); // this removes the file according to tmpfile() docs
    }

注意!如果您的代码抛出意外的异常/致命错误或仅包含一些语法错误,则不会执行 "cleanup" 块。

"where" 块

“Where” 块是一个特殊块,其中包含所谓的“参数化”,这是一种在多个数据集上执行一个特定规范的方法。它与 phpUnit 的“数据集”非常相似,但更好,因为参数化还可以使用 "setup" 块中定义的变量。

注意!理解以下内容非常重要,即具有参数化的测试将多次从上到下执行,包括 "setup" 和 "cleanup" 块。只有数据在执行之间不同。

参数化有两种语法(或表示法)。一种是数组样式

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationArrayNotation()
    {
        /**
         * @var $a
         */

        expect:
        $a + 2 > 0;

        where:
        $a << array(1, 2, 3);
    }

在这里,您表示规范将执行三次,$a 将包含数组(1,2,3)中的每个值

并且相同的表格样式

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationTableNotation()
    {
        /**
         * @var $a
         * @var $b
         * @var $c
         */

        expect:
        $a + $b == $c;

        where:
        $a  | $b  | $c;
         1  |  2  |  3;
         3  |  2  |  5;
         3  |  4  |  7;
        -3  |  4  |  1;
    }

当您需要分配多个变量时,这更好。解析器将把这个表格转换成

<?php

         $a << array(1, 3, 3, -3);
         $b << array(2, 2, 4,  4);
         $c << array(3, 5, 7,  1);

每个表格行应包含与表头(第一行)相同数量的列,并且表中各行的行之间不应有空行。值和分隔符之间空格的数量并不重要。

您会注意到最后两个测试中包含变量声明的 doc-block 注释

<?php

    /**
     * @var $a
     * @var $b
     * @var $c
     */

这告诉您的 IDE 这些变量将动态创建,IDE 不会对未定义的变量发出警告。您还可以添加变量类型,并获取漂亮的自动完成功能

<?php

    /**
     * @var $stack \Example\Stack
     * @var $b
     * @var $c
     */

您可以在一个测试中将参数化的表和数组表示法组合在一起

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationMixedNotation()
    {
        /**
         * @var $a
         * @var $b
         * @var $c
         * @var $d
         * @var $e
         * @var $f
         */

        expect:
        $a + $b + $c + $d + $e + $f > 0;

        where:
        $a  | $b  | $c;
         1  |  2  |  3;
         3  |  2  |  5;
         3  |  4  |  7;
        -3  |  4  |  1;

        $d << array(1, 2, 3);

        $e  | $f;
         2  |  3;
         2  |  5;
    }

并且如果某些参数化语句具有不同数量的值,则值将滚动

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationValueRolling()
    {
        /**
         * @var $a
         * @var $b
         */

        expect:
        $a + $b > 0;

        where:
        $a << array(1, 2, 3);
        $b << array(1, 2);
    }

结果如下组合

<?php

    $a: 1, $b: 1
    $a: 2, $b: 2
    $a: 3, $b: 1

迭代次数将是最大参数化中的元素数量。

您还可以使用在 "setup" 语句中定义的任何变量

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationVariablesFromSetup()
    {
        /**
         * @var $a
         */
        setup:
        $b = 123;

        expect:
        $a + 1  > 100;

        where:
        $a << array($b + 1, $b + 3, 101);
    }

    /**
     * @spec
     * @test
     */
    public function parametrizationVariablesFromSetupInTable()
    {
        /**
         * @var $a
         * @var $c
         */
        setup:
        $b = 123;

        expect:
        $a + $c + 1  > 100;

        where:
        $a      | $c;
        $b + 1  | 1 ;
        2       | $b + 3;
        101     | 3;
    }

甚至可以使用某些外部方法或变量作为参数化值来源

<?php

    /**
     * @spec
     * @test
     */
    public function parametrizationWithExternalValueSource()
    {
        /**
         * @var $word
         */
        setup:
        $myDataProvider = function() {
            return explode(' ', 'When in the Course of human events it becomes necessary for one people to dissolve the political bands which have connected them with another and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature\'s God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.');
        };

        expect:
        preg_match('/[a-zA-Z]{1,15}/', $word) == true;

        where:
        $word << $myDataProvider();
    }

这里我们测试给定的文本是否只包含至少包含一个英语字符的单词。

如果存在断言错误,当前参数化参数也将添加到错误消息中。让我们稍微修改一下测试,以查看具有参数化参数的断言错误是什么样的

<?php

    ...
    preg_match('/^[a-zA-Z]{1,15}$/', $word) == true;
    ...

以下是输出

There was 1 failure:

1) DocExamples\SpecificationSyntaxTest::parametrizationWithExternalValueSource
Expression preg_match(\'/^[a-zA-Z]{1,15}$/\', $word) == true is evaluated to false.

 Where:
---------------------------------------------------
  $word :  'earth,'


 Parametriazation values [step 32]:
---------------------------------------------------
 $word :  element[32] of array: $myDataProvider()


 Declared variables:
---------------------------------------------------
 $myDataProvider  : instance of Closure
 $word            : earth,

---------------------------------------------------

我们可以清楚地看到单词 "earth," 以逗号结尾,并且它没有通过正则表达式。

顺便说一下,数据提供者也可以是测试类的公共方法

<?php

        ...
        where:
        $word << $this->myDataProvider();
        ...

测试异常

您可以通过几种方式测试异常。第一种是 phpUnit 的方式

<?php
    /**
     * @spec
     * @expectedException Exception
     */
    public function testIndex()
    {
        when:
        throw new \Exception("test");

        then:
        $this->fail("Exception should be thrown!");
    }

更好的方法是使用 thrown() 和 notThrown() 结构

<?php

    /**
     * @spec
     */
    public function testIndexWithThrown()
    {
        when:
        throw new \Exception("test");

        then:
        thrown("Exception");
    }

到目前为止,我还没有找到告诉 IDE thrown() 和 notThrown() 函数存在的方法。因此,目前 IDE(至少是 phpStorm)对这些方法会发出“未定义函数”的警告。

thrown() 接受类名作为参数,如果没有提供参数,则默认为 'Exception'。

thrown() 将检查 "when" 块是否抛出了异常,如果没有,则使用断言错误失败。

<?php

    /**
     * @spec
     */
    public function testIndexWithThrown3()
    {
        when:
        throw new \RuntimeException("test");

        then:
        thrown("RuntimeException");
    }

输出结果将是

There was 1 failure:

1) MyExamples\ExceptionExampleTest::testIndexWithThrown3
Expression thrown("RuntimeException") is evaluated to false.

notThrown()可以使测试更加完整。在任何情况下,如果发生异常,测试都将失败,但如果你在“then”块中有notThrown()语句,你的测试目的将更加明确。

此外,每个规范至少应包含一个“then”(或“expect”)块,且不能为空。

thrown()和notThrown()断言仅适用于最后一个“when”块。这允许你做一些像这样的操作

<?php

    /**
     * @spec
     */
    public function testExceptionCombination()
    {
        when:
        1==1;

        then:
        notThrown("RuntimeException");

        _when:
        throw new \RuntimeException("test");

        _then:
        thrown("RuntimeException");
    }

如果你的设置块中发生异常,你的测试失败是合乎逻辑的。

模拟和交互

PhpSpock底层使用Mockery模拟框架,但它的DSL被采纳以符合Spock风格。

要创建一个模拟对象,你应在规范的开头创建一个do块,并声明变量。在声明中,第一个参数应该是类或接口名称,第二个是“Mock”关键字,这告诉解析器该变量应该被模拟。

这种@var风格的声明对于模拟来说很好,因为IDE将为你提供模拟的自动完成。通常情况下,使用Mockery原生创建模拟时不会出现这种情况。

以下是一个示例

<?php

    /**
     * @spec
     */
    public function test1()
    {
        /**
         * @var $a \Example\Calc *Mock*
         */
        setup:
        1 * $a->add(_,_);

        when:
        $a->add(1,2);

        then:
        notThrown();
    }

在这里,我们定义了一个新的类型为\Example\Calc的模拟,并在设置块中声明,方法“add()”应被调用一次,带有两个任意参数。

构造“1 * $a->add(,”);”被称为交互,可以插入到设置块或then块中。

在设置块中,你可以声明测试范围内的交互,通常这些是对可选方法的返回值声明

<?php

    /**
     * @spec
     */
    public function testSetupBlockInteractions()
    {
        /**
         * @var $a \Example\Calc *Mock*
         */
        setup:
        (0.._) * $a->add(1, 2) >> 3;
        (0.._) * $a->add(2, 2) >> 4;

        when:
        $b = $a->add(1,2);

        then:
        $b == 3;
    }

当然,某些其他类会调用你的模拟方法,但为了说明发生了什么,上面的代码片段是很好的。

然后在then方法中,你通常声明你对模拟方法调用次数的期望

<?php

    /**
     * @spec
     */
    public function test3()
    {
        /**
         * @var $a \Example\Calc *Mock*
         */

        when:
        $b = $a->add(1,2);

        then:
        1 * $a->add(_,_) >> 4;
        $b == 4;
    }

以下是交互声明语法

{Cardinality} * ${mockVarName}->{mockedMethodName}([{argument declaration}]) [ >> {return value declaration}]

基数

基数是期望调用次数的确切数字,如“1”,“2”或0,或区间

(n.._) * subscriber.receive(event) // at least n times

(_..n) * subscriber.receive(event) // at most n times

(m..n) * subscriber.receive(event) // between m and n times

区间的替代语法

+n * subscriber.receive(event) // at least n times

-n * subscriber.receive(event) // at most n times

m..n * subscriber.receive(event) // between m and n times

例如

+0 * $a->add(_,_) >> throws('RuntimeException', 'foo');

mockVarName和mockedMethodName

仅仅是字符串。

参数声明

格式是:arg1, arg2, .... argN

特殊格式是:*,它表示方法可以带有任意数量的参数。

参数是

  • 一个常量,如:1, 2, "some string", WHATEVER_CONTANT ...等等。与"=="比较
  • $variable name - 将按引用检查
  • "任何值(对于定义参数数量很有用:如,,,_)"
  • something([some params])将被转换为\Mockery::something(some params),请参考Mockery文档 [https://github.com/padraic/mockery]

返回值

  • 一个常量
  • usingClosure(function(){}) - 闭包将接收方法接收到的参数
  • throw(ExceptionInstance) - 方法将抛出一个异常
  • 一个变量

更多示例

<?php

    /**
     * @spec
     */
    public function test9()
    {
        /**
         * @var $a \Example\Calc *Mock*
         */
        setup:
        1 * $a->add(_,_) >> throws('RuntimeException', 'foo');

        when:
        $b = $a->add(1,2);

        then:
        thrown('RuntimeException');
    }

共享资源

注意!非常重要,你必须声明你打算在测试中使用的所有资源为公共的。否则,你的规范将无法调用这些资源,因为测试将在不同的上下文中执行。

与phpUnit的调试器支持

有时在交互式调试器中调试你的测试很有用。例如,在你的IDE中使用xdebug。

PhpSpoc规范通常生成测试代码并使用eval执行它。所以通常的方式你不能在规范方法上设置断点。

在这种情况下,如果你使用phpUnitAdapter,只需添加@specDebug注释(除了现有的@spec注释外),PhpSpock将在你的规范方法旁边生成本机phpUnit testCase方法。这个方法将被内部phpSpock内容淹没,但它将解决问题。每次运行你的测试时,这个生成的测试都将被执行,因此你可以在代码上设置断点。

在您处理完bug并愿意摆脱这些糟糕的自动生成代码后,只需移除注解@specDebug,phpSpec会为您清理测试。

此外,@specDebug可能有助于理解phpSpock的内部机制。例如,如果您对测试中的某些难以理解的行为感到困惑,并认为PhpSpock运行不正确。