Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. BDD

Понимание PhpSpec

by
Difficulty:AdvancedLength:LongLanguages:

Russian (Pусский) translation by Masha Kolesnikova (you can also view the original English article)

Если вы сравните PhpSpec с другими фреймворками тестирования, вы обнаружите, что это очень сложный и упрямый инструмент. Одной из причин этого является то, что PhpSpec не является фреймворком тестирования, как те, которые вы уже знаете.

Вместо этого это инструмент проектирования, который помогает описать поведение программного обеспечения. Побочным эффектом описания поведения программного обеспечения с PhpSpec является то, что вы получите спецификации, которые также будут служить в качестве тестов впоследствии.

В этой статье мы заглянем под капот PhpSpec и попытаемся получить более глубокое представление о том, как он работает и как его использовать.

Если вы хотите освежить в памяти, что такое phpspec, взгляните на мой учебник по началу работы.

В этой статье...

  • Быстрый обзор внутренностей PhpSpec
  • Разница между TDD и BDD
  • Чем отличается PhpSpec (от PHPUnit)
  • PhpSpec: инструмент проектирования

Быстрый обзор внутренностей PhpSpec

Давайте начнем с изучения некоторых ключевых понятий и классов, которые формируют PhpSpec.

Понимание $this

Понимание того, что означает $this, является ключевым для понимания того, чем PhpSpec отличается от других инструментов. В принципе, $this ссылается на экземпляр фактического тестируемого класса. Попробуем углубиться в это немного больше, чтобы лучше понять, что мы имеем в виду.

Прежде всего, нам нужна спецификация и класс для тестирования. Как вы знаете, генераторы PhpSpec сделают это очень легко для нас:

Затем, откройте созданный файл spec и попробуем получить немного больше информации о $this:

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

Хорошо, и не удивительно, что get_class() говорит нам, что $this экземпляр spec\Suhm\HelloWorldSpec. Это имеет смысл, поскольку, в конце концов, это просто обычный PHP-код. Если вместо этого мы использовали get_parent_class(), мы бы получили PhpSpec\ObjectBehavior, так как наша спецификация расширяет этот класс.

Помните, я только что сказал вам, что $this действительно относится к тестируемому классу, который в нашем случае был бы Suhm\HelloWorld? Как вы можете видеть, возвращаемое значение get_class ($this) противоречит $this->shouldHaveType('Suhm\HelloWorld');.

Давайте попробуем что-нибудь еще:

С помощью вышеуказанного кода мы пытаемся вызвать метод с именем dumpThis() в экземпляре HelloWorld. Мы добавим ожидание к вызову метода, ожидая, что возвращаемое значение функции будет строкой, содержащей «spec\Suhm\HelloWorldSpec». Это возвращаемое значение из get_class() в строке выше.

Опять же, генераторы PhpSpec могут помочь нам с некоторым предварительным кодом:

Попробуем также вызвать get_class() из dumpThis():

Опять же, неудивительно, что мы получаем:

Похоже, что мы чего-то здесь не замечаем. Я начал с того, что сказал, что $this не относится к тому, что вы думаете, но пока наши эксперименты не показали ничего неожиданного. За исключением одного: как мы могли бы назвать $this->dumpThis()?

Чтобы понять это, нам нужно погрузиться в исходный код PhpSpec. Если вы хотите, вы можете прочитать код на GitHub.

Посмотрите следующий код из src/PhpSpec/ObjectBehavior.php (класс, который расширяет наша спецификация):

Комментарии дают большую часть информации: "Proxies all call to the PhpSpec subject". Метод PHP __call - это волшебный метод, который вызывается автоматически, когда метод недоступен (или не существует).

Это означает, что когда мы пытались вызвать $this->dumpThis(), вызов, по-видимому, был проксирован к объекту PhpSpec. Если вы посмотрите на код, вы увидите, что вызов метода проксирован в $this->object. (То же самое относится и к свойствам в нашем экземпляре. Все они также связаны с объектом, используя другие магические методы. Посмотрите в исходном коде, чтобы убедиться.)

Давайте проверим get_class() еще раз и посмотрим, что он должен сказать о $this->object:

И посмотрим, что получилось:

Еще о Subject

Subject - это оболочка и реализует PhpSpec\Wrapper\WrapperInterface. Это ключевая часть PhpSpec и позволяет использовать все [казалось бы] магии, которые предоставляет фреймворк. Она содержит в себе экземпляр класса, который мы тестируем, чтобы мы могли делать всевозможные вещи, такие как вызовы методов и свойств, которые не существуют и устанавливать ожидания.

Как уже упоминалось, PhpSpec очень ориентирован на то, как вы должны писать и специфицировать свой код. Один spec сопоставляется с одним классом. У вас есть только один subject для каждой спецификации. Важно отметить, что это позволяет вам использовать $this, как если бы это был фактический экземпляр, и делает его действительно понятным и содержательным.

PhpSpec содержит Wrapper, который занимается созданием Subject. Он упаковывает Subject с фактическим объектом, который мы специфицируем. Поскольку Subject реализует WrapperInterface, он должен иметь метод getWrappedObject(), который дает нам доступ к объекту. Это экземпляр объекта, который мы искали ранее с помощью get_class().

Попробуем еще раз:

И вот вы идете:

Несмотря на то, что многое происходит за сценой, в конце мы все еще работаем с фактическим экземпляром объекта Suhm\HelloWorld. Отлично.

Раньше, когда мы вызывали $this->dumpThis(), мы узнали, как вызов был фактически проксирован для Subject. Мы также узнали, что Subject является только оболочкой, а не фактическим объектом.

Благодаря этим знаниям ясно, что мы не можем вызвать dumpThis() на Subject без другого магического метода. Subject имеет метод __call():

Этот метод делает две вещи. Во-первых, он проверяет, начинается ли имя метода с 'should'. Если это так, это ожидание, и вызов делегируется методу callExpectation(). Если нет, вызов вместо этого делегируется экземпляру PhpSpec\Wrapper\Subject\Caller.

На данный момент мы будем игнорировать Caller. Он также содержит обернутый объект и знает, как вызвать на нем методы. Caller возвращает завернутый экземпляр, когда он вызывает методы subject, что позволяет нам связывать ожидания с методами, как это было сделано в случае с dumpThis().

Вместо этого давайте посмотрим на метод callExpectation():

Этот метод отвечает за создание экземпляра PhpSpec\Wrapper\Subject\Expectation\ExpectationInterface. Этот интерфейс определяет метод match(), который callExpectation() вызывает для проверки ожидания. Существует четыре разных вида ожиданий: Positive,  NegativePositiveThrow и NegativeThrow. Каждый из этих ожиданий содержит экземпляр PhpSpec\Matcher\MatcherInterface, который использует метод match(). Давайте посмотрим на ближайших помощников.

Matchers

Матчи - это то, что мы используем для определения поведения наших объектов. Всякий раз, когда мы пишем, should ... или shouldNot ..., мы используем матч. Вы можете найти полный список помощников PhpSpec в моем личном блоге.

В PhpSpec есть много шаблонов, которые расширяют класс PhpSpec\Matcher\BasicMatcher, который реализует MatcherInterface. То, как работают матчи, довольно просто. Давайте взглянем на это вместе, и я также рекомендую вам взглянуть на исходный код.

В качестве примера рассмотрим этот код из IdentityMatcher:

Метод supports() определен в MatcherInterface. В этом случае для матча в массиве $keywords определены четыре псевдонима. Это позволит матчеру поддерживать либо: shouldReturn(), shouldBe(), shouldEqual() или shouldBeEqualTo(), либо shouldNotReturn(), shouldNotBe(), shouldNotEqual() или shouldNotBeEqualTo().

Из BasicMatcher наследуются два метода: positiveMatch() и negativeMatch(). Они выглядят так:

Метод positiveMatch() генерирует исключение, если метод matches() (абстрактный метод, который должны реализовывать все соответствующие классы) возвращает false. Метод negativeMatch() работает обратным образом. Метод matches() для theIdentityMatcher использует оператор === для сравнения $subject с аргументом, предоставленным методу сравнения:

Мы могли бы использовать этот помощник следующим образом:

Что в конечном итоге вызовет функцию negativeMatch() и убедится, что match() возвращает false.

Взгляните на некоторых других помощников и посмотрите, что они делают!

Обещания большего волшебства

Прежде чем мы закончим этот короткий тур по внутренним частям PhpSpec, давайте посмотрим на еще одну магию:

Добавляя в наш пример тайп хинт для параметра $object, PhpSpec автоматически использует reflection для инъекции экземпляра этого класса. Но с тем, что мы уже видели, действительно ли мы верим, что мы действительно получаем экземпляр StdClass? Давайте проверим еще раз с помощью get_class():

Нет. Вместо StdClass мы получаем экземпляр PhpSpec\Wrapper\Collaborator. Что это за класс?

Подобно Subject, Collaborator - это оболочка и реализует WrapperInterface. Он содержит внутри экземпляр \Prophecy\Prophecy\ObjectProphecy, который проистекает из Prophecy, mock фреймворка, который поставляется вместе с PhpSpec. Вместо экземпляра StdClass PhpSpec дает нам мок. Что делает использование моков в PhpSpec предельно простым и позволяет добавлять обещания к нашим объектам следующим образом:

В этом коротком туре по части внутренних компонентов PhpSpec я надеюсь, что вы увидели, что это больше, чем простой фреймворк для тестирования.

Разница между TDD и BDD

PhpSpec - это инструмент для выполнения SpecBDD, поэтому, чтобы лучше понять, давайте рассмотрим различия между разработкой, основанной на тестах (TDD) и разработкой, основанной на поведении (BDD). После этого мы быстро рассмотрим, чем PhpSpec отличается от других инструментов, таких как PHPUnit.

TDD - это концепция, позволяющая автоматическим тестам управлять дизайном и внедрением кода. Путем написания небольших тестов для каждой функции, прежде чем их реализовать, когда мы получим проходящий тест, мы знаем, что наш код удовлетворяет этой конкретной функции. После прохождения теста, после рефакторинга, мы прекращаем кодирование и вместо этого записываем следующий тест. Известная мантра «красный», «зеленый», «рефакторинг»!

BDD имеет свое происхождение от - и очень похож на - TDD. Честно говоря, речь идет главным образом о формулировке, которая действительно важна, поскольку она может изменить то, как мы думаем во время разработки. В тот момент, когда TDD рассказывает о тестировании, BDD рассказывает об описании поведения.

С TDD мы фокусируемся на проверке того, что наш код работает так, как мы ожидаем, что он будет работать, тогда как с BDD мы сосредоточимся на проверке того, что наш код действительно ведет себя так, как мы этого хотим. Основной причиной появления BDD в качестве альтернативы TDD является отказ от использования слова «тест». С BDD нам не очень интересно тестировать реализацию нашего кода, нас больше интересует тестирование того, что он делает (его поведение). Когда мы используем BDD, вместо TDD, у нас есть истории и спецификации. Это упрощает запись традиционных тестов.

Истории и спецификации тесно связаны с ожиданиями участников проекта. Написание историй (с помощью такого инструмента, как Behat), предпочтительно будет взаимодействовать вместе с заинтересованными сторонами или экспертами в доменной области. Истории охватывают внешнее поведение. Мы используем спецификации для разработки внутреннего поведения, необходимого для полного заполнения этапов истории. Каждый шаг в истории может потребовать нескольких итераций с написанием спецификаций и внедрением кода, прежде чем спецификация будет удовлетворена. Наши истории вместе с нашими спецификациями помогают нам убедиться, что мы не только строим рабочую вещь, но и то, что она также и правильная. Таким образом, BDD имеет много общего с коммуникацией.

Чем PhpSpec отличается от PHPUnit?

Несколько месяцев назад заметный член сообщества PHP, Mathias Verraes, разместил пост «Тестовый модуль в твитте» в Twitter. Дело было в том, чтобы подгонять исходный код функциональной модульной тестовой платформы в один твит. Как видно из сути, код действительно функциональный и позволяет писать базовые модульные тесты. Концепция модульного тестирования на самом деле довольно проста: проверьте какое-то утверждение и сообщите пользователю о результате.

Конечно, большинство тестовых фреймворков, таких как PHPUnit, действительно намного более продвинуты и могут делать гораздо больше, чем структура Mathias, но она по-прежнему показывает важный момент: вы утверждаете что-то, а затем ваш фреймворк запускает это утверждение для вас.

Давайте рассмотрим очень простой тест PHPUnit:

Сможете ли вы написать суперпростую реализацию фреймворка тестирования, которая могла бы запустить этот тест? Я уверен, что ответ «да», вы можете это сделать. В конце концов, единственное, что должен сделать метод assertTrue(), - это сравнить значение с true и выбросить исключение, если оно не выполнено. В основе всего, что происходит, на самом деле довольно прямолинейно.

Итак, чем же отличается PhpSpec? Прежде всего, PhpSpec не является инструментом тестирования. Тестирование вашего кода не является основной целью PhpSpec, но это становится побочным эффектом, если вы используете его для разработки своего программного обеспечения путем постепенного добавления спецификаций для поведения (BDD).

Во-вторых, я думаю, что в вышеуказанных разделах должно было быть ясно, чем отличается PhpSpec. Тем не менее, давайте сравним некоторый код:

Поскольку PhpSpec очень самоуверен и высказывает некоторые утверждения о том, как разрабатывается наш код, он дает нам очень простой способ описать наш код. С другой стороны, PHPUnit не делает никаких утверждений в отношении нашего кода и позволяет нам делать в значительной степени то, что мы хотим. В принципе, все PHPUnit для нас в этом примере - это запуск объекта $object с оператором instanceof.

Несмотря на то, что с PHPUnit может показаться проще начать работу (я не думаю, что это так), если вы не будете осторожны, вы можете легко попасть в ловушки плохого дизайна и архитектуры, потому что он позволяет вам делать что угодно. При этом PHPUnit все еще может быть отличным для многих случаев использования, но это не инструмент разработки, такой как PhpSpec. Нет руководства - вы должны знать, что делаете.

PhpSpec: инструмент проектирования

На веб-сайте PhpSpec мы можем узнать, что PhpSpec:

Набор инструментов php для создания дизайна по спецификации.

Позвольте мне сказать это еще раз: PhpSpec не является фреймворком тестирования. Это инструмент разработки. Инструмент разработки программного обеспечения. Это не простой фреймворк с утверждениями, который сравнивает значения и бросает исключения. Это инструмент, который помогает нам в разработке и создании хорошо продуманного кода. Он требует от нас мыслить о структуре нашего кода и применять некоторые архитектурные шаблоны, где один класс сопоставляется с одной спецификацией. Если вы нарушите принцип единой ответственности и вам нужно частично что-то замокать, вам не разрешат это делать.

Удачи со спецификациями!

Ой! И, наконец, =, поскольку сам PhpSpec специфицирован, я предлагаю вам перейти на GitHub и изучить исходный код, чтобы узнать больше.

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.