Статьи

Начало работы с Phpspec

В этом коротком, но всеобъемлющем учебнике мы рассмотрим разработку, основанную на поведении (BDD), с использованием phpspec . В основном, это будет введение в инструмент phpspec, но по ходу мы затронем различные концепции BDD. В наши дни BDD является горячей темой, и в последнее время phpspec привлекла большое внимание сообщества PHP.

BDD — это описание поведения программного обеспечения для правильного проектирования. Это часто связано с TDD, но в то время как TDD фокусируется на тестировании вашего приложения, BDD больше посвящен описанию его поведения . Использование подхода BDD заставит вас постоянно учитывать фактические требования и желаемое поведение программного обеспечения, которое вы создаете.

В последнее время в сообществе PHP большое внимание привлекли два инструмента BDD, Behat и phpspec . Behat помогает вам описать внешнее поведение вашего приложения, используя читаемый язык корнишонов. phpspec, с другой стороны, помогает вам описать внутреннее поведение вашего приложения, написав небольшие «спецификации» на языке PHP — отсюда SpecBDD. Эти спецификации проверяют, что ваш код имеет желаемое поведение.

В этом уроке мы рассмотрим все, что связано с началом работы с phpspec. На нашем пути мы шаг за шагом создадим основу приложения списка задач, используя подход SpecBDD. По ходу дела у нас будет phpspec!

Примечание: это промежуточная статья о PHP. Я предполагаю, что вы хорошо разбираетесь в объектно-ориентированном PHP.

Для этого урока я предполагаю, что у вас есть следующие компоненты:

Установка phpspec через Composer является самым простым способом. Все, что вам нужно сделать, это запустить следующую команду в терминале:

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

Это создаст для вас файл composer.json и установит phpspec в каталог vendor/ .

Чтобы убедиться, что все работает, запустите phpspec и убедитесь, что вы получите следующий вывод:

1
2
3
4
5
$ vendor/bin/phpspec run
 
0 specs
0 examples
0ms

Прежде чем мы начнем, нам нужно сделать немного конфигурации. Когда запускается phpspec, он ищет файл YAML с именем phpspec.yml . Поскольку мы будем помещать наш код в пространство имен, мы должны убедиться, что phpspec знает об этом. Кроме того, пока мы это делаем, давайте удостоверимся, что наши спецификации выглядят красиво и красиво, когда мы их запускаем.

Сделайте файл со следующим содержимым:

1
2
3
4
formatter.name: pretty
suites:
    todo_suite:
        namespace: Petersuhm\Todo

Есть много других доступных вариантов конфигурации, о которых вы можете прочитать в документации .

Еще одна вещь, которую нам нужно сделать, это сообщить Composer, как автоматически загружать наш код. phpspec будет использовать автозагрузчик Composer, поэтому это необходимо для запуска наших спецификаций.

Добавьте элемент автозагрузки в файл composer.json который Composer сделал для вас:

01
02
03
04
05
06
07
08
09
10
{
    «require»: {
        «phpspec/phpspec»: «2.0.*@dev»
    },
    «autoload»: {
        «psr-0»: {
            «Petersuhm\\Todo»: «src»
        }
    }
}

Запуск composer dump-autoload обновит автозагрузчик после этого изменения.

Теперь мы готовы написать нашу первую спецификацию. Мы начнем с описания класса с именем TaskCollection . Мы заставим phpspec сгенерировать для нас класс spec с помощью команды description (или, альтернативно, короткой версии desc ).

1
2
3
$ vendor/bin/phpspec describe «Petersuhm\Todo\TaskCollection»
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TaskCollection` for you?

Так что здесь произошло? Сначала мы попросили phpspec создать спецификацию для TaskCollection . Во-вторых, мы запустили наш набор спецификаций, а затем phpspec автоматически предложили создать для нас TaskCollection класс TaskCollection . Круто, не правда ли?

Продолжайте и снова запустите пакет, и вы увидите, что у нас уже есть один пример в нашей спецификации (через минуту мы увидим, что это за пример):

01
02
03
04
05
06
07
08
09
10
$ vendor/bin/phpspec run
 
      Petersuhm\Todo\TaskCollection
 
  10 ✔ is initializable
 
 
1 specs
1 examples (1 passed)
7ms

Из этого вывода мы видим, что TaskCollection инициализируется . О чем это? Посмотрите на файл spec, сгенерированный phpspec, и он должен быть более понятным:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?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_initializable() » происходит от функции it_is_initializable() которую phpspec добавил в класс с именем TaskCollectionSpec . Эта функция — то, что мы называем примером . В этом конкретном примере у нас есть то, что мы называем shouldHaveType() именем shouldHaveType() который проверяет тип нашей TaskCollection . Если вы измените параметр, переданный этой функции, на что-то другое и снова запустите спецификацию, вы увидите, что она не будет выполнена. Прежде чем полностью понять это, я думаю, что нам нужно выяснить, на что указывает переменная $this в нашей спецификации.

Конечно, $this относится к экземпляру класса TaskCollectionSpec , поскольку это всего лишь обычный код PHP. Но с phpspec вы должны рассматривать $this иначе, чем вы обычно делаете, поскольку под капотом он фактически ссылается на тестируемый объект, который на самом деле является классом TaskCollection . Это поведение унаследовано от класса ObjectBehavior , который обеспечивает прокси-вызовы функций для указанного класса. Это означает, что SomeClassSpec будет прокси-вызовы методов для экземпляра SomeClass . phpspec обернет эти вызовы методов для запуска их возвращаемых значений в сопоставлениях, подобных тому, который вы только что видели.

Вам не нужно глубоко понимать это, чтобы использовать phpspec, просто помните, что, насколько вам известно, $this самом деле относится к тестируемому объекту.

Пока что мы ничего не сделали сами. Но phpspec TaskCollection для нас пустой класс TaskCollection . Теперь пришло время заполнить некоторый код и сделать этот класс полезным. Мы добавим два метода: метод add() для добавления задач и метод count() для подсчета количества задач в коллекции.

Прежде чем писать какой-либо реальный код, мы должны написать пример в нашей спецификации. В нашем примере мы хотим попытаться добавить задачу в коллекцию, а затем убедиться, что задача действительно добавлена. Чтобы сделать это, нам нужен экземпляр (пока не существующий) класса Task . Если мы добавим эту зависимость в качестве параметра в нашу функцию spec, phpspec автоматически предоставит нам экземпляр, который мы можем использовать. На самом деле, это не настоящий экземпляр, но то, что phpspec называет Collaborator . Этот объект будет действовать как реальный объект, но phpspec позволяет нам делать более причудливые вещи с этим, что мы скоро увидим. Несмотря на то, что класс Task еще не существует, пока просто притворяйтесь, что он существует. Откройте TaskCollectionSpec и добавьте оператор use для класса Task а затем добавьте пример it_adds_a_task_to_the_collection() :

1
2
3
4
5
6
7
8
9
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:

1
2
3
$ vendor/bin/phpspec describe «Petersuhm\Todo\Task»
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\Task` for you?

При повторном запуске phpspec происходит нечто интересное:

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

Отлично! Если вы посмотрите на файл TaskCollection.php , то увидите, что phpspec создал функцию add() мы должны заполнить:

01
02
03
04
05
06
07
08
09
10
11
12
<?php
 
namespace Petersuhm\Todo;
 
class TaskCollection
{
 
    public function add($argument1)
    {
        // TODO: write logic here
    }
}

phpspec все еще жалуется. У нас нет массива $tasks , поэтому давайте создадим его и добавим в него задачу:

01
02
03
04
05
06
07
08
09
10
11
12
13
<?php
 
namespace Petersuhm\Todo;
 
class TaskCollection
{
    public $tasks;
 
    public function add(Task $task)
    {
        $this->tasks[] = $task;
    }
}

Теперь наши спецификации все хорошие и зеленые. Обратите внимание, что я обязательно напечатал подсказку параметра $task .

Просто чтобы убедиться, что мы правильно поняли, давайте добавим еще одну задачу:

1
2
3
4
5
6
7
8
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, похоже, у нас все хорошо.

Мы хотим знать, сколько задач находится в коллекции, что является отличной причиной для использования одного из интерфейсов из стандартной библиотеки PHP (SPL), а именно интерфейса Countable . Этот интерфейс диктует, что реализующий его класс должен иметь метод count() .

Ранее мы использовали shouldHaveType() , который является сопоставителем типов . Он использует компаратор PHP instanceof для проверки того, что объект на самом деле является экземпляром данного класса. Есть 4 типа соответствия, которые делают то же самое. Одним из них является shouldImplement() , который идеально подходит для наших целей, поэтому давайте продолжим и используем это в примере:

1
2
3
4
function it_is_countable()
{
    $this->shouldImplement(‘Countable’);
}

Видишь, как красиво это звучит? Давайте запустим пример и предложим phpspec:

1
2
3
4
5
$ vendor/bin/phpspec run
 
       Petersuhm/Todo/TaskCollection
 25 ✘ is countable
       expected an instance of Countable, but got [obj:Petersuhm\Todo\TaskCollection].

Итак, наш класс не является экземпляром Countable так как мы еще не реализовали его. Давайте обновим код для нашего класса TaskCollection :

1
class TaskCollection implements \Countable

Наши тесты не будут выполняться, поскольку в интерфейсе Countable есть абстрактный метод count() , который мы должны реализовать. Пустой метод покажет цели:

1
2
3
4
public function count()
{
    // …
}

И мы вернулись к зеленому. На данный момент наш метод count() мало что делает, и на самом деле он довольно бесполезен. Давайте напишем спецификацию поведения, которое мы хотим иметь. Во-первых, без задач наша функция count должна вернуть ноль:

1
2
3
4
function it_counts_elements_of_the_collection()
{
    $this->count()->shouldReturn(0);
}

Возвращает null , а не 0 . Чтобы получить зеленый тест, давайте исправим этот способ TDD / BDD:

1
2
3
4
public function count()
{
    return 0;
}

Мы зеленые и все хорошо, но это, вероятно, не то поведение, которое мы хотим. Вместо этого давайте расширим нашу спецификацию и добавим что-то в массив $tasks :

1
2
3
4
5
6
7
function it_counts_elements_of_the_collection()
{
    $this->count()->shouldReturn(0);
 
    $this->tasks = [‘foo’];
    $this->count()->shouldReturn(1);
}

Конечно, наш код все еще возвращает 0 , и у нас есть красный шаг. Исправить это не слишком сложно, и TaskCollection наш класс TaskCollection должен выглядеть следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?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 класс, назовем его TodoList , который может использовать наш класс коллекции.

1
2
3
$ vendor/bin/phpspec desc «Petersuhm\Todo\TodoList»
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList` for you?

Первый пример, который мы добавим, это один для добавления задач. Мы сделаем метод addTask() , который не делает ничего, кроме добавления задачи в нашу коллекцию. Он просто направляет вызов метода add() в коллекции, так что это идеальное место для использования ожидания. Мы не хотим, чтобы метод фактически вызывал метод add() , мы просто хотим убедиться, что он пытается это сделать. Кроме того, мы хотим убедиться, что он вызывает его только один раз. Посмотрите, как мы можем сделать это с помощью phpspec:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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() должен вызываться ровно 1 раз с переменной $task в качестве параметра». Так мы готовим нашего соавтора, который теперь является имитацией, прежде чем назначить его свойству $tasks в TodoList . Наконец, мы пытаемся вызвать метод addTask() .

Хорошо, что phpspec должен сказать по этому поводу:

1
2
3
4
5
$ vendor/bin/phpspec run
 
       Petersuhm/Todo/TodoList
 17 !
       property tasks not found.

Свойство $tasks не существует — просто:

1
2
3
4
5
6
7
8
<?php
 
namespace Petersuhm\Todo;
 
class TodoList
{
    public $tasks;
}

Попробуйте еще раз, и пусть phpspec поможет нам:

01
02
03
04
05
06
07
08
09
10
11
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList::addTask()` for you?
$ 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() , заполните следующий код в пустом addTask() :

01
02
03
04
05
06
07
08
09
10
11
12
13
<?php
 
namespace Petersuhm\Todo;
 
class TodoList
{
    public $tasks;
 
    public function addTask(Task $task)
    {
        $this->tasks->add($task);
    }
}

Вернуться к зеленому! Это хорошо, правда?

Давайте посмотрим на обещания тоже. Нам нужен метод, который может сообщить нам, есть ли какие-либо задачи в коллекции. Для этого мы просто проверим возвращаемое значение метода count() в коллекции. Опять же, нам не нужен реальный экземпляр с реальным методом count() . Нам просто нужно убедиться, что наш код вызывает некоторый метод count() и выполняет некоторые действия в зависимости от возвращаемого значения.

Взгляните на следующий пример:

1
2
3
4
5
6
7
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 должен сказать по этому поводу?

1
2
3
4
5
6
7
$ vendor/bin/phpspec run
Do you want me to create `Petersuhm\Todo\TodoList::hasTasks()` for you?
$ vendor/bin/phpspec run
 
        Petersuhm/Todo/TodoList
  25 ✘ checks whether it has any tasks
        expected false, but got null.

Здорово. phpspec сделал нас hasTasks() и неудивительно, что он возвращает null , а не false .

Еще раз, это легко исправить:

1
2
3
4
public function hasTasks()
{
    return false;
}

Мы вернулись к зеленому, но это не совсем то, что мы хотим. Давайте проверим задачи, когда их 20. Это должно вернуть true :

01
02
03
04
05
06
07
08
09
10
11
12
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, и мы получим:

1
2
3
4
5
$ vendor/bin/phpspec run
 
       Petersuhm/Todo/TodoList
 25 ✘ checks whether it has any tasks
       expected true, but got false.

Хорошо, false не true , поэтому нам нужно улучшить наш код. Давайте используем этот метод count() чтобы увидеть, есть ли задачи или нет:

1
2
3
4
5
6
7
public function hasTasks()
{
    if ($this->tasks->count() > 0)
        return true;
 
    return false;
}

Тах да! Вернуться к зеленому!

Часть написания хороших спецификаций — сделать их максимально читабельными. Наш последний пример может быть улучшен чуть-чуть, благодаря пользовательским сопоставителям phpspec. Легко реализовать пользовательские сопоставления — все, что нам нужно сделать, это переписать метод getMatchers() , унаследованный от ObjectBehavior . Благодаря реализации двух пользовательских сопоставлений наша спецификация может быть изменена следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
        },
    ];
}

Я думаю, это выглядит довольно хорошо. Помните, что рефакторинг ваших спецификаций важен для их актуальности. Реализация собственных пользовательских сопоставлений может очистить ваши спецификации и сделать их более читабельными.

На самом деле, мы можем использовать отрицание совпадений:

01
02
03
04
05
06
07
08
09
10
11
12
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();
}

Да. Довольно круто!

Все наши спецификации зеленые и посмотрите, как хорошо они документируют наш код!

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 в мой рабочий процесс я действительно чувствую обратное — моя производительность значительно улучшилась. И мой код более солидный!