aleczhang/phpspock

PhpSpock是Spock测试框架的PHP实现

0.1.4 2015-01-26 08:59 UTC

This package is not auto-updated.

Last update: 2024-09-28 16:36:20 UTC


README

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上fork存储库,并提交拉取请求以合并你的更改到主分支。但请记住,PhpSpock核心应该对任何类型的测试框架一无所知,并使用事件系统与之交互。

用户指南

简介

如你所知,PhpSpock是Spock测试框架的克隆版,因此你也可以阅读SpockBasics文档以了解更多关于两个框架底层理念的信息:[http://code.google.com/p/spock/wiki/SpockBasics].

术语

"Spock 允许您编写描述感兴趣系统预期特性(属性、方面)的规范。感兴趣的系统可以是单个类到整个应用程序之间的任何内容,也称为规范下的系统(SUS)。特性的描述从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"块一起工作。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"块

块是一个特殊的块,包含所谓的“参数化”,它是在不同数据集上执行特定规格的方法。它非常类似于phpUnit的“数据集”,但更好,因为参数化还可以使用设置块中定义的变量。

注意!重要的是理解,具有参数化的测试将从顶部到底部执行多次,包括设置和清理块。只有数据在执行之间会不同。

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

<?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

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

您还可以使用设置语句中定义的任何变量

<?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风格。

要创建一个模拟对象,您应该在规格的开始处创建一个doc块,并声明变量。在声明中,第一个参数应该是类或接口名称,第二个是"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;
    }

当然,其他类可能会调用您的模拟方法,但为了说明正在发生的事情,上面的代码片段是很好的。

然后方法通常用于声明对模拟方法调用次数的预期。

<?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的内容,但它会解决问题。每次您运行测试时,这个生成的测试都将执行,因此您可以在代码上设置断点。

在您解决了所有错误并想要摆脱这个糟糕的生成代码后,只需移除 @specDebug 注解,phpSpec 将为您清理测试。

此外,@specDebug 有助于理解phpSpock的内部结构。例如,如果您对测试的某些行为感到困惑,并认为 PhpSpock 的工作方式不正确。