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 сделают это очень легко для нас:
$ phpspec desc "Suhm\HelloWorld" $ phpspec run Do you want me to create `Suhm\HelloWorld` for you? y
Затем, откройте созданный файл spec и попробуем получить немного больше информации о $this
:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this)); } }
get_class()
возвращает имя класса для данного объекта. В этом случае мы просто передаем $this
, чтобы увидеть, что он возвращает:
$ string(24) "spec\Suhm\HelloWorldSpec"
Хорошо, и не удивительно, что get_class()
говорит нам, что $this
экземпляр spec\Suhm\HelloWorldSpec
. Это имеет смысл, поскольку, в конце концов, это просто обычный PHP-код. Если вместо этого мы использовали get_parent_class()
, мы бы получили PhpSpec\ObjectBehavior
, так как наша спецификация расширяет этот класс.
Помните, я только что сказал вам, что $this
действительно относится к тестируемому классу, который в нашем случае был бы Suhm\HelloWorld
? Как вы можете видеть, возвращаемое значение get_class ($this)
противоречит $this->shouldHaveType('Suhm\HelloWorld');
.
Давайте попробуем что-нибудь еще:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this)); $this->dumpThis()->shouldReturn('spec\Suhm\HelloWorldSpec'); } }
С помощью вышеуказанного кода мы пытаемся вызвать метод с именем dumpThis()
в экземпляре HelloWorld
. Мы добавим ожидание к вызову метода, ожидая, что возвращаемое значение функции будет строкой, содержащей «spec\Suhm\HelloWorldSpec
». Это возвращаемое значение из get_class()
в строке выше.
Опять же, генераторы PhpSpec могут помочь нам с некоторым предварительным кодом:
$ phpspec run Do you want me to create `Suhm\HelloWorld::dumpThis()` for you? y
Попробуем также вызвать get_class()
из dumpThis()
:
<?php namespace Suhm; class HelloWorld { public function dumpThis() { return get_class($this); } }
Опять же, неудивительно, что мы получаем:
10 ✘ it is initializable expected "spec\Suhm\HelloWorldSpec", but got "Suhm\HelloWorld".
Похоже, что мы чего-то здесь не замечаем. Я начал с того, что сказал, что $this
не относится к тому, что вы думаете, но пока наши эксперименты не показали ничего неожиданного. За исключением одного: как мы могли бы назвать $this->dumpThis()
?
Чтобы понять это, нам нужно погрузиться в исходный код PhpSpec. Если вы хотите, вы можете прочитать код на GitHub.
Посмотрите следующий код из src/PhpSpec/ObjectBehavior.php
(класс, который расширяет наша спецификация):
/** * Proxies all call to the PhpSpec subject * * @param string $method * @param array $arguments * * @return mixed */ public function __call($method, array $arguments = array()) { return call_user_func_array(array($this->object, $method), $arguments); }
Комментарии дают большую часть информации: "Proxies all call to the PhpSpec subject"
. Метод PHP __call
- это волшебный метод, который вызывается автоматически, когда метод недоступен (или не существует).
Это означает, что когда мы пытались вызвать $this->dumpThis()
, вызов, по-видимому, был проксирован к объекту PhpSpec. Если вы посмотрите на код, вы увидите, что вызов метода проксирован в $this->object
. (То же самое относится и к свойствам в нашем экземпляре. Все они также связаны с объектом, используя другие магические методы. Посмотрите в исходном коде, чтобы убедиться.)
Давайте проверим get_class()
еще раз и посмотрим, что он должен сказать о $this->object
:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this->object)); } }
И посмотрим, что получилось:
string(23) "PhpSpec\Wrapper\Subject"
Еще о Subject
Subject
- это оболочка и реализует PhpSpec\Wrapper\WrapperInterface
. Это ключевая часть PhpSpec и позволяет использовать все [казалось бы] магии, которые предоставляет фреймворк. Она содержит в себе экземпляр класса, который мы тестируем, чтобы мы могли делать всевозможные вещи, такие как вызовы методов и свойств, которые не существуют и устанавливать ожидания.
Как уже упоминалось, PhpSpec очень ориентирован на то, как вы должны писать и специфицировать свой код. Один spec сопоставляется с одним классом. У вас есть только один subject для каждой спецификации. Важно отметить, что это позволяет вам использовать $this
, как если бы это был фактический экземпляр, и делает его действительно понятным и содержательным.
PhpSpec содержит Wrapper
, который занимается созданием Subject
. Он упаковывает Subject
с фактическим объектом, который мы специфицируем. Поскольку Subject
реализует WrapperInterface
, он должен иметь метод getWrappedObject()
, который дает нам доступ к объекту. Это экземпляр объекта, который мы искали ранее с помощью get_class()
.
Попробуем еще раз:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($this->object->getWrappedObject())); // And just to be completely sure: var_dump($this->object->getWrappedObject()->dumpThis()); } }
И вот вы идете:
$ vendor/bin/phpspec run string(15) "Suhm\HelloWorld" string(15) "Suhm\HelloWorld"
Несмотря на то, что многое происходит за сценой, в конце мы все еще работаем с фактическим экземпляром объекта Suhm\HelloWorld
. Отлично.
Раньше, когда мы вызывали $this->dumpThis()
, мы узнали, как вызов был фактически проксирован для Subject
. Мы также узнали, что Subject
является только оболочкой, а не фактическим объектом.
Благодаря этим знаниям ясно, что мы не можем вызвать dumpThis()
на Subject
без другого магического метода. Subject
имеет метод __call()
:
/** * @param string $method * @param array $arguments * * @return mixed|Subject */ public function __call($method, array $arguments = array()) { if (0 === strpos($method, 'should')) { return $this->callExpectation($method, $arguments); } return $this->caller->call($method, $arguments); }
Этот метод делает две вещи. Во-первых, он проверяет, начинается ли имя метода с 'should'. Если это так, это ожидание, и вызов делегируется методу callExpectation()
. Если нет, вызов вместо этого делегируется экземпляру PhpSpec\Wrapper\Subject\Caller
.
На данный момент мы будем игнорировать Caller
. Он также содержит обернутый объект и знает, как вызвать на нем методы. Caller
возвращает завернутый экземпляр, когда он вызывает методы subject, что позволяет нам связывать ожидания с методами, как это было сделано в случае с dumpThis().
Вместо этого давайте посмотрим на метод callExpectation()
:
/** * @param string $method * @param array $arguments * * @return mixed */ private function callExpectation($method, array $arguments) { $subject = $this->makeSureWeHaveASubject(); $expectation = $this->expectationFactory->create($method, $subject, $arguments); if (0 === strpos($method, 'shouldNot')) { return $expectation->match(lcfirst(substr($method, 9)), $this, $arguments, $this->wrappedObject); } return $expectation->match(lcfirst(substr($method, 6)), $this, $arguments, $this->wrappedObject); }
Этот метод отвечает за создание экземпляра PhpSpec\Wrapper\Subject\Expectation\ExpectationInterface
. Этот интерфейс определяет метод match()
, который callExpectation()
вызывает для проверки ожидания. Существует четыре разных вида ожиданий: Positive
, Negative
, PositiveThrow
и NegativeThrow
. Каждый из этих ожиданий содержит экземпляр PhpSpec\Matcher\MatcherInterface
, который использует метод match()
. Давайте посмотрим на ближайших помощников.
Matchers
Матчи - это то, что мы используем для определения поведения наших объектов. Всякий раз, когда мы пишем, should ...
или shouldNot ...
, мы используем матч. Вы можете найти полный список помощников PhpSpec в моем личном блоге.
В PhpSpec есть много шаблонов, которые расширяют класс PhpSpec\Matcher\BasicMatcher
, который реализует MatcherInterface.
То, как работают матчи, довольно просто. Давайте взглянем на это вместе, и я также рекомендую вам взглянуть на исходный код.
В качестве примера рассмотрим этот код из IdentityMatcher
:
/** * @var array */ private static $keywords = array( 'return', 'be', 'equal', 'beEqualTo' ); /** * @param string $name * @param mixed $subject * @param array $arguments * * @return bool */ public function supports($name, $subject, array $arguments) { return in_array($name, self::$keywords) && 1 == count($arguments) ; }
Метод supports()
определен в MatcherInterface
. В этом случае для матча в массиве $keywords
определены четыре псевдонима. Это позволит матчеру поддерживать либо: shouldReturn()
, shouldBe()
, shouldEqual()
или shouldBeEqualTo()
, либо shouldNotReturn()
, shouldNotBe()
, shouldNotEqual()
или shouldNotBeEqualTo()
.
Из BasicMatcher
наследуются два метода: positiveMatch()
и negativeMatch()
. Они выглядят так:
/** * @param string $name * @param mixed $subject * @param array $arguments * * @return mixed * * @throws FailureException */ final public function positiveMatch($name, $subject, array $arguments) { if (false === $this->matches($subject, $arguments)) { throw $this->getFailureException($name, $subject, $arguments); } return $subject; }
Метод positiveMatch()
генерирует исключение, если метод matches()
(абстрактный метод, который должны реализовывать все соответствующие классы) возвращает false
. Метод negativeMatch()
работает обратным образом. Метод matches()
для theIdentityMatcher
использует оператор ===
для сравнения $subject
с аргументом, предоставленным методу сравнения:
/** * @param mixed $subject * @param array $arguments * * @return bool */ protected function matches($subject, array $arguments) { return $subject === $arguments[0]; }
Мы могли бы использовать этот помощник следующим образом:
$this->getUser()->shouldNotBeEqualTo($anotherUser);
Что в конечном итоге вызовет функцию negativeMatch(
) и убедится, что match()
возвращает false.
Взгляните на некоторых других помощников и посмотрите, что они делают!
Обещания большего волшебства
Прежде чем мы закончим этот короткий тур по внутренним частям PhpSpec, давайте посмотрим на еще одну магию:
<?php namespace spec\Suhm; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class HelloWorldSpec extends ObjectBehavior { function it_is_initializable(\StdClass $object) { $this->shouldHaveType('Suhm\HelloWorld'); var_dump(get_class($object)); } }
Добавляя в наш пример тайп хинт для параметра $object
, PhpSpec автоматически использует reflection для инъекции экземпляра этого класса. Но с тем, что мы уже видели, действительно ли мы верим, что мы действительно получаем экземпляр StdClass
? Давайте проверим еще раз с помощью get_class()
:
$ vendor/bin/phpspec run string(28) "PhpSpec\Wrapper\Collaborator"
Нет. Вместо StdClass
мы получаем экземпляр PhpSpec\Wrapper\Collaborator
. Что это за класс?
Подобно Subject
, Collaborator
- это оболочка и реализует WrapperInterface
. Он содержит внутри экземпляр \Prophecy\Prophecy\ObjectProphecy
, который проистекает из Prophecy, mock фреймворка, который поставляется вместе с PhpSpec. Вместо экземпляра StdClass
PhpSpec дает нам мок. Что делает использование моков в PhpSpec предельно простым и позволяет добавлять обещания к нашим объектам следующим образом:
$user->getAge()->willReturn(10); $this->setUser($user); $this->getUserStatus()->shouldReturn('child');
В этом коротком туре по части внутренних компонентов 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:
public function testTrue() { $this->assertTrue(false); }
Сможете ли вы написать суперпростую реализацию фреймворка тестирования, которая могла бы запустить этот тест? Я уверен, что ответ «да», вы можете это сделать. В конце концов, единственное, что должен сделать метод assertTrue()
, - это сравнить значение с true
и выбросить исключение, если оно не выполнено. В основе всего, что происходит, на самом деле довольно прямолинейно.
Итак, чем же отличается PhpSpec? Прежде всего, PhpSpec не является инструментом тестирования. Тестирование вашего кода не является основной целью PhpSpec, но это становится побочным эффектом, если вы используете его для разработки своего программного обеспечения путем постепенного добавления спецификаций для поведения (BDD).
Во-вторых, я думаю, что в вышеуказанных разделах должно было быть ясно, чем отличается PhpSpec. Тем не менее, давайте сравним некоторый код:
// PhpSpec function it_is_initializable() { $this->shouldHaveType('Suhm\HelloWorld'); } // PHPUnit function testIsInitializable() { $object = new Suhm\HelloWorld(); $this->assertInstanceOf('Suhm\HelloWorld', $object); }
Поскольку PhpSpec очень самоуверен и высказывает некоторые утверждения о том, как разрабатывается наш код, он дает нам очень простой способ описать наш код. С другой стороны, PHPUnit не делает никаких утверждений в отношении нашего кода и позволяет нам делать в значительной степени то, что мы хотим. В принципе, все PHPUnit для нас в этом примере - это запуск объекта $object
с оператором instanceof
.
Несмотря на то, что с PHPUnit может показаться проще начать работу (я не думаю, что это так), если вы не будете осторожны, вы можете легко попасть в ловушки плохого дизайна и архитектуры, потому что он позволяет вам делать что угодно. При этом PHPUnit все еще может быть отличным для многих случаев использования, но это не инструмент разработки, такой как PhpSpec. Нет руководства - вы должны знать, что делаете.
PhpSpec: инструмент проектирования
На веб-сайте PhpSpec мы можем узнать, что PhpSpec:
Набор инструментов php для создания дизайна по спецификации.
Позвольте мне сказать это еще раз: PhpSpec не является фреймворком тестирования. Это инструмент разработки. Инструмент разработки программного обеспечения. Это не простой фреймворк с утверждениями, который сравнивает значения и бросает исключения. Это инструмент, который помогает нам в разработке и создании хорошо продуманного кода. Он требует от нас мыслить о структуре нашего кода и применять некоторые архитектурные шаблоны, где один класс сопоставляется с одной спецификацией. Если вы нарушите принцип единой ответственности и вам нужно частично что-то замокать, вам не разрешат это делать.
Удачи со спецификациями!
Ой! И, наконец, =, поскольку сам PhpSpec специфицирован, я предлагаю вам перейти на GitHub и изучить исходный код, чтобы узнать больше.
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Update me weeklyEnvato Tuts+ tutorials are translated into other languages by our community members—you can be involved too!
Translate this post