Home  >  Article  >  Backend Development  >  Phpspec: A Beginner’s Guide to Getting Started

Phpspec: A Beginner’s Guide to Getting Started

PHPz
PHPzOriginal
2023-08-31 14:29:02661browse

Phpspec: A Beginner’s Guide to Getting Started

在这个简短而全面的教程中,我们将了解 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() 方法,用于计算集合中的任务数量。

添加任务

在编写任何实际代码之前,我们应该在规范中编写一个示例。在我们的示例中,我们想要尝试将任务添加到集合中,然后确保该任务确实已添加。为此,我们需要一个(目前还不存在的)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 纳入我的工作流程后,我确实感觉到了相反的情况 - 我的生产力显着提高。而且我的代码更扎实!

The above is the detailed content of Phpspec: A Beginner’s Guide to Getting Started. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn