卓越飞翔博客卓越飞翔博客

卓越飞翔 - 您值得收藏的技术分享站
技术文章16928本站已运行3322

Phpspec:初学者入门指南

Phpspec:初学者入门指南

在这个简短而全面的教程中,我们将了解 phpspec 的行为驱动开发 (BDD)。大多数情况下,它将介绍 phpspec 工具,但随着我们的讨论,我们将触及不同的 BDD 概念。 BDD 是当今的热门话题,phpspec 最近在 PHP 社区中获得了广泛关注。

SpecBDD 和 Phpspec

BDD 旨在描述软件的行为,以便获得正确的设计。它通常与 TDD 相关,但 TDD 专注于测试您的应用程序,而 BDD 更多的是描述其行为。使用 BDD 方法将迫使您不断考虑正在构建的软件的实际需求和期望的行为。

最近,两个 BDD 工具在 PHP 社区中获得了广泛关注,Behat 和 phpspec。 Behat 可帮助您使用可读的 Gherkin 语言描述应用程序的外部行为。另一方面,phpspec 通过用 PHP 语言编写小的“规范”来帮助您描述应用程序的内部行为 - 因此是 SpecBDD。这些规范正在测试您的代码是否具有所需的行为。

我们将做什么

在本教程中,我们将介绍与 phpspec 入门相关的所有内容。在此过程中,我们将使用 SpecBDD 方法逐步构建待办事项列表应用程序的基础。在我们前进的过程中,我们将让 phpspec 引领我们!

注意:这是一篇关于 PHP 的中级文章。我假设您已经很好地掌握了面向对象的 PHP。

安装

对于本教程,我假设您已启动并运行以下内容:

  • 有效的 PHP 设置(最低 5.3)
  • 作曲家

通过 Composer 安装 phpspec 是最简单的方法。您所要做的就是在终端中运行以下命令:

$ composer require phpspec/phpspec
Please provide a version constraint for the phpspec/phpspec requirement: 2.0.*@dev

这将为您创建一个 composer.json 文件,并将 phpspec 安装在 vendor/ 目录中。

为了确保一切正常,请运行 phpspec 并查看您获得以下输出:

$ vendor/bin/phpspec run

0 specs
0 examples 
0ms

配置

在开始之前,我们需要做一些配置。当 phpspec 运行时,它会查找名为 phpspec.yml 的 YAML 文件。由于我们将把代码放在命名空间中,因此我们需要确保 phpspec 知道这一点。另外,在我们这样做的同时,让我们确保我们的规格在运行时看起来很漂亮。

继续创建包含以下内容的文件:

formatter.name: pretty
suites:
    todo_suite:
        namespace: Petersuhm\Todo

还有许多其他可用的配置选项,您可以在文档中阅读。

我们需要做的另一件事是告诉 Composer 如何自动加载我们的代码。 phpspec 将使用 Composer 的自动加载器,因此这是我们规范运行所必需的。

将自动加载元素添加到 Composer 为您创建的 composer.json 文件中:

{
    "require": {
        "phpspec/phpspec": "2.0.*@dev"
    },
    "autoload": {
        "psr-0": {
            "Petersuhm\\Todo": "src"
        }
    }
}

运行 composer dump-autoload 将在此更改后更新自动加载器。

我们的第一个规范

现在我们准备编写我们的第一个规范。我们首先描述一个名为 TaskCollection 的类。我们将使用 describe 命令(或者简短版本 desc)让 phpspec 为我们生成一个规范类。

$ vendor/bin/phpspec describe "Petersuhm\Todo\TaskCollection"
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TaskCollection` for you? y

那么这里发生了什么?首先,我们要求 phpspec 为 TaskCollection 创建规范。其次,我们运行我们的规范套件,然后 phpspec 自动为我们提供创建实际的 TaskCollection 类。很酷,不是吗?

继续并再次运行该套件,您将看到我们的规范中已经有一个示例(我们稍后将看到示例是什么):

$ vendor/bin/phpspec run

      Petersuhm\Todo\TaskCollection

  10  ✔ is initializable


1 specs
1 examples (1 passed)
7ms

从此输出中,我们可以看到 TaskCollection 已初始化。这是关于什么的?看一下phpspec生成的spec文件,应该就更清楚了:

<?php

namespace spec\Petersuhm\Todo;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class TaskCollectionSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(\'Petersuhm\Todo\TaskCollection\');
    }
}

短语“可初始化”源自名为 it_is_initializes() 的函数,该函数 phpspec 已添加到名为 TaskCollectionSpec 的类中。这个函数就是我们所说的示例。在这个特定的示例中,我们有一个称为 shouldHaveType()匹配器,它检查 TaskCollection 的类型。如果将传递给该函数的参数更改为其他参数并再次运行规范,您将看到它将失败。在完全理解这一点之前,我认为我们需要研究变量 $this 在我们的规范中指的是什么。

什么是 $this

当然,$this 指的是 TaskCollectionSpec 类的实例,因为这只是常规 PHP 代码。但是对于 phpspec,您必须将 $this 与您通常所做的不同,因为在幕后,它实际上指的是被测试的对象,这实际上是 TaskCollection 类。此行为继承自类 ObjectBehavior,它确保函数调用被代理到指定的类。这意味着 SomeClassSpec 将代理方法调用到 SomeClass 的实例。 phpspec 将包装这些方法调用,以便针对您刚刚看到的匹配器运行它们的返回值。

为了使用 phpspec,您不需要深入了解这一点,只需记住,就您而言,$this 实际上指的是被测试的对象。

构建我们的任务集合

到目前为止,我们自己还没有做任何事情。但是 phpspec 制作了一个空的 TaskCollection 类供我们使用。现在是时候填写一些代码并使此类变得有用了。我们将添加两个方法:一个 add() 方法,用于添加任务;一个 count() 方法,用于计算集合中的任务数量。< /p>

添加任务

在编写任何实际代码之前,我们应该在规范中编写一个示例。在我们的示例中,我们想要尝试将任务添加到集合中,然后确保该任务确实已添加。为此,我们需要一个(目前还不存在的)Task 类的实例。如果我们将此依赖项作为参数添加到我们的spec函数中,phpspec将自动为我们提供一个可以使用的实例。实际上,该实例并不是真正的实例,而是 phpspec 所说的 Collaborator。该对象将充当真实对象,但 phpspec 允许我们用它做更多奇特的事情,我们很快就会看到。尽管 Task 类尚不存在,但现在就假装它存在。打开 TaskCollectionSpec 并为 Task 类添加 use 语句,然后添加示例 it_adds_a_task_to_the_collection()

use Petersuhm\Todo\Task;

...

function it_adds_a_task_to_the_collection(Task $task)
{
    $this->add($task);
    $this->tasks[0]->shouldBe($task);
}

在我们的示例中,我们编写了“我们希望有”的代码。我们调用 add() 方法,然后尝试给它一个 $task。然后我们检查该任务实际上是否已添加到实例变量 $tasks 中。匹配器 shouldBe() 是一个身份 匹配器,类似于 PHP === 比较器。您可以使用 shouldBe()shouldBeEqualTo()shouldEqual()shouldReturn() - 他们都做同样的事情。

运行 phpspec 会产生一些错误,因为我们还没有名为 Task 的类。

让 phpspec 帮我们解决这个问题:

$ vendor/bin/phpspec describe "Petersuhm\Todo\Task"
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\Task` for you? y

再次运行 phpspec,发生了一些有趣的事情:

$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TaskCollection::add()` for you? y

完美!如果你看一下 TaskCollection.php 文件,你会发现 phpspec 制作了一个 add() 函数供我们填写:

<?php

namespace Petersuhm\Todo;

class TaskCollection
{

    public function add($argument1)
    {
        // TODO: write logic here
    }
}

尽管如此,phpspec 仍然在抱怨。我们没有 $tasks 数组,所以让我们创建一个数组并向其中添加任务:

<?php

namespace Petersuhm\Todo;

class TaskCollection
{
    public $tasks;

    public function add(Task $task)
    {
        $this->tasks[] = $task;
    }
}

现在我们的规格都很好而且绿色。请注意,我确保输入了 $task 参数。

为了确保我们做得正确,让我们添加另一个任务:

function it_adds_a_task_to_the_collection(Task $task, Task $anotherTask)
{
    $this->add($task);
    $this->tasks[0]->shouldBe($task);

    $this->add($anotherTask);
    $this->tasks[1]->shouldBe($anotherTask);
}

运行 phpspec,看起来一切都很好。

实现 Countable 接口

我们想知道一个集合中有多少个任务,这是使用标准 PHP 库 (SPL) 中的一个接口(即 Countable 接口)的一个重要原因。此接口规定实现它的类必须具有 count() 方法。

之前,我们使用了匹配器 shouldHaveType(),它是一个类型匹配器。它使用 PHP 比较器 instanceof 来验证对象实际上是给定类的实例。有 4 个类型匹配器,它们的作用都相同。其中之一是 shouldImplement(),它非常适合我们的目的,所以让我们继续在示例中使用它:

function it_is_countable()
{
    $this->shouldImplement(\'Countable\');
}

看到这句话读起来多漂亮了吗?让我们运行该示例并让 phpspec 为我们引路:

$ vendor/bin/phpspec run

        Petersuhm/Todo/TaskCollection
  25  ✘ is countable
        expected an instance of Countable, but got [obj:Petersuhm\Todo\TaskCollection].

好吧,我们的类不是 Countable 的实例,因为我们还没有实现它。让我们更新 TaskCollection 类的代码:

class TaskCollection implements \Countable

我们的测试将无法运行,因为 Countable 接口有一个抽象方法 count(),我们必须实现该方法。现在,一个空方法就可以解决问题:

public function count()
{
    // ...
}

我们又回到了绿色。目前,我们的 count() 方法没有做太多事情,而且实际上没什么用。让我们为我们希望它具有的行为编写一个规范。首先,在没有任务的情况下,我们的计数函数预计返回零:

function it_counts_elements_of_the_collection()
{
    $this->count()->shouldReturn(0);
}

它返回 null,而不是 0。为了获得绿色测试,让我们用 TDD/BDD 方式解决这个问题:

public function count()
{
    return 0;
}

我们是绿色的,一切都很好,但这可能不是我们想要的行为。相反,让我们扩展规范并向 $tasks 数组添加一些内容:

function it_counts_elements_of_the_collection()
{
    $this->count()->shouldReturn(0);

    $this->tasks = [\'foo\'];
    $this->count()->shouldReturn(1);
}

当然,我们的代码仍然返回 0,并且我们有一个红色步骤。解决这个问题并不太困难,我们的 TaskCollection 类现在应该如下所示:

<?php

namespace Petersuhm\Todo;

class TaskCollection implements \Countable
{
    public $tasks;

    public function add(Task $task)
    {
        $this->tasks[] = $task;
    }

    public function count()
    {
        return count($this->tasks);
    }
}

我们进行了绿色测试,我们的 count() 方法有效。多么美好的一天!

期望与承诺

还记得我告诉过你,phpspec 允许你使用 Collaborator 类的实例(又名由 phpspec 自动注入的实例)做一些很酷的事情吗?如果您以前编写过单元测试,您就会知道模拟和存根是什么。如果没有,请不要太担心。这只是行话。这些东西指的是“假”对象,它们将充当您的真实对象,但允许您单独进行测试。如果您的规范中需要,phpspec 会自动将这些 Collaborator 实例转换为模拟和存根。

这真是太棒了。在底层,phpspec 使用 Prophecy 库,这是一个高度固执己见的模拟框架,与 phpspec 配合得很好(并且是由同样出色的人构建的)。您可以对协作者设置期望(模拟),例如“这个方法应该被调用”,并且您可以添加承诺(存根),例如“这个方法将返回”这个值”。有了 phpspec,这真的很容易,接下来我们将完成这两件事。

让我们创建一个类,我们将其命名为 TodoList,它可以使用我们的集合类。

$ vendor/bin/phpspec desc "Petersuhm\Todo\TodoList"
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList` for you? y

添加任务

我们要添加的第一个示例是用于添加任务的示例。我们将创建一个 addTask() 方法,该方法只不过是向我们的集合添加一个任务。它只是将调用定向到集合上的 add() 方法,因此这是利用期望的完美位置。我们不希望该方法实际调用 add() 方法,我们只是想确保它尝试执行此操作。此外,我们希望确保它只调用一次。看看我们如何使用 phpspec 来解决这个问题:

<?php

namespace spec\Petersuhm\Todo;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Petersuhm\Todo\TaskCollection;
use Petersuhm\Todo\Task;

class TodoListSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(\'Petersuhm\Todo\TodoList\');
    }

    function it_adds_a_task_to_the_list(TaskCollection $tasks, Task $task)
    {
        $tasks->add($task)->shouldBeCalledTimes(1);
        $this->tasks = $tasks;

        $this->addTask($task);
    }
}

首先,我们让 phpspec 为我们提供了我们需要的两个协作者:一个任务集合和一个任务。然后,我们对任务收集协作者设置一个期望,基本上是这样的:“add() 方法应该以变量 $task 作为参数调用恰好 1 次” 。这就是我们如何准备我们的协作者(现在是一个模拟),然后将其分配给 TodoList 上的 $tasks 属性。最后,我们尝试实际调用 addTask() 方法。

好吧,phpspec 对此有何评论:

$ vendor/bin/phpspec run

        Petersuhm/Todo/TodoList
  17  ! adds a task to the list
        property tasks not found.

$tasks 属性不存在 - 简单的一个:

<?php

namespace Petersuhm\Todo;

class TodoList
{
    public $tasks;
}

再试一次,让 phpspec 指导我们:

$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList::addTask()` for you? y
$ vendor/bin/phpspec run

        Petersuhm/Todo/TodoList
  17  ✘ adds a task to the list
        some predictions failed:
          Double\Petersuhm\Todo\TaskCollection\P4:
            Expected exactly 1 calls that match:
              Double\Petersuhm\Todo\TaskCollection\P4->add(exact(Double\Petersuhm\Todo\Task\P3:000000002544d76d0000000059fcae53))
            but none were made.

好吧,现在发生了一些有趣的事情。看到消息“预计有 1 个匹配的呼叫:...”?这是我们失败的期望。发生这种情况是因为在调用 addTask() 方法后,集合上的 add() 方法没有被调用,这正是我们所期望的是。

为了恢复绿色,请在空的 addTask() 方法中填写以下代码:

<?php

namespace Petersuhm\Todo;

class TodoList
{
    public $tasks;

    public function addTask(Task $task)
    {
        $this->tasks->add($task);
    }
}

回到绿色!感觉不错吧?

检查任务

我们也来看看 Promise。我们需要一个方法来告诉我们集合中是否有任何任务。为此,我们只需检查集合上 count() 方法的返回值。同样,我们不需要具有真实 count() 方法的真实实例。我们只需要确保我们的代码调用一些 count() 方法并根据返回值执行一些操作。

看一下下面的示例:

function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
{
    $tasks->count()->willReturn(0);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldReturn(false);
}

我们有一个任务收集协作者,它有一个 count() 方法,将返回零。这是我们的承诺。这意味着每次有人调用 count() 方法时,它都会返回零。然后,我们将准备好的协作者分配给对象的 $tasks 属性。最后,我们尝试调用一个方法 hasTasks(),并确保它返回 false

phspec 对此有什么看法?

$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList::hasTasks()` for you? y
$ vendor/bin/phpspec run

        Petersuhm/Todo/TodoList
  25  ✘ checks whether it has any tasks
        expected false, but got null.

酷。 phpspec 为我们创建了一个 hasTasks() 方法,毫不奇怪,它返回 null,而不是 false

再一次强调,这是一个很容易解决的问题:

public function hasTasks()
{
    return false;
}

我们又回到了绿色,但这并不是我们想要的。当任务有 20 个时,我们来检查一下。这应该返回 true:

function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
{
    $tasks->count()->willReturn(0);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldReturn(false);

    $tasks->count()->willReturn(20);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldReturn(true);
}

运行 phspec 我们会得到:

$ vendor/bin/phpspec run

        Petersuhm/Todo/TodoList
  25  ✘ checks whether it has any tasks
        expected true, but got false.

好吧,false 不是 true,所以我们需要改进我们的代码。让我们使用 count() 方法来查看是否有任务:

public function hasTasks()
{
    if ($this->tasks->count() > 0)
        return true;

    return false;
}

哒哒!回到绿色!

构建自定义匹配器

编写好的规范的一部分是使其尽可能具有可读性。由于 phpspec 的自定义匹配器,我们的最后一个示例实际上可以得到一点点改进。实现自定义匹配器很容易 - 我们所要做的就是覆盖从 ObjectBehavior 继承的 getMatchers() 方法。通过实现两个自定义匹配器,我们的规范可以更改为如下所示:

function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
{
    $tasks->count()->willReturn(0);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldBeFalse();

    $tasks->count()->willReturn(20);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldBeTrue();
}

function getMatchers()
{
    return [
        \'beTrue\' => function($subject) {
            return $subject === true;
        },
        \'beFalse\' => function($subject) {
            return $subject === false;
        },
    ];
}

我觉得这个看起来不错。请记住,重构您的规范对于使其保持最新状态非常重要。实现您自己的自定义匹配器可以清理您的规范并使其更具可读性。

实际上,我们也可以使用匹配器的否定:

function it_checks_whether_it_has_any_tasks(TaskCollection $tasks)
{
    $tasks->count()->willReturn(0);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldNotBeTrue();

    $tasks->count()->willReturn(20);
    $this->tasks = $tasks;

    $this->hasTasks()->shouldNotBeFalse();
}

是的。非常酷!

结论

我们所有的规范都是绿色的,看看它们如何很好地记录我们的代码!

      Petersuhm\Todo\TaskCollection

  10  ✔ is initializable
  15  ✔ adds a task to the collection
  24  ✔ is countable
  29  ✔ counts elements of the collection

      Petersuhm\Todo\Task

  10  ✔ is initializable

      Petersuhm\Todo\TodoList

  11  ✔ is initializable
  16  ✔ adds a task to the list
  24  ✔ checks whether it has any tasks


3 specs
8 examples (8 passed)
16ms

我们已经有效地描述并实现了代码的预期行为。更不用说,我们的代码 100% 符合我们的规范,这意味着重构不会是一种令人恐惧的体验。

通过跟随,我希望您能受到启发来尝试 phpspec。它不仅仅是一个测试工具——它还是一个设计工具。一旦您习惯了使用 phpspec(及其出色的代码生成工具),您将很难再次放弃它!人们经常抱怨 TDD 或 BDD 降低了他们的速度。将 phpspec 纳入我的工作流程后,我确实感觉到了相反的情况 - 我的生产力显着提高。而且我的代码更扎实!

卓越飞翔博客
上一篇: 如何使用Python-Plotly制作基本的散点图?
下一篇: 返回列表
留言与评论(共有 0 条评论)
   
验证码:
隐藏边栏