在这个简短而全面的教程中,我们将了解 phpspec 的行为驱动开发 (BDD)。大多数情况下,它将介绍 phpspec 工具,但随着我们的讨论,我们将触及不同的 BDD 概念。 BDD 是当今的热门话题,phpspec 最近在 PHP 社区中获得了广泛关注。
BDD 旨在描述软件的行为,以便获得正确的设计。它通常与 TDD 相关,但 TDD 专注于测试您的应用程序,而 BDD 更多的是描述其行为。使用 BDD 方法将迫使您不断考虑正在构建的软件的实际需求和期望的行为。
最近,两个 BDD 工具在 PHP 社区中获得了广泛关注,Behat 和 phpspec。 Behat 可帮助您使用可读的 Gherkin 语言描述应用程序的外部行为。另一方面,phpspec 通过用 PHP 语言编写小的“规范”来帮助您描述应用程序的内部行为 - 因此是 SpecBDD。这些规范正在测试您的代码是否具有所需的行为。
在本教程中,我们将介绍与 phpspec 入门相关的所有内容。在此过程中,我们将使用 SpecBDD 方法逐步构建待办事项列表应用程序的基础。在我们前进的过程中,我们将让 phpspec 引领我们!
注意:这是一篇关于 PHP 的中级文章。我假设您已经很好地掌握了面向对象的 PHP。
对于本教程,我假设您已启动并运行以下内容:
通过 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 纳入我的工作流程后,我确实感觉到了相反的情况 - 我的生产力显着提高。而且我的代码更扎实!
以上是Phpspec:初學者入門指南的詳細內容。更多資訊請關注PHP中文網其他相關文章!