Статьи

Понимание PhpSpec

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

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

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

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

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

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

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

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

1
2
3
$ phpspec desc «Suhm\HelloWorld»
$ phpspec run
Do you want me to create `Suhm\HelloWorld` for you?

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?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 туда, чтобы увидеть, что он возвращает:

1
$ 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'); ,

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<?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 могут помочь нам с некоторыми лесами:

1
2
$ phpspec run
Do you want me to create `Suhm\HelloWorld::dumpThis()` for you?

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

01
02
03
04
05
06
07
08
09
10
11
12
<?php
 
namespace Suhm;
 
class HelloWorld
{
 
    public function dumpThis()
    {
        return get_class($this);
    }
}

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

1
2
10 ✘ it is initializable
     expected «spec\Suhm\HelloWorldSpec», but got «Suhm\HelloWorld».

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * 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 :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?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));
    }
}

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

1
string(23) «PhpSpec\Wrapper\Subject»

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

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

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

Давайте попробуем это снова:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?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());
    }
}

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

1
2
3
$ vendor/bin/phpspec run
string(15) «Suhm\HelloWorld»
string(15) «Suhm\HelloWorld»

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
/**
 * @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);
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * @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() . Давайте посмотрим на совпадения дальше.

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

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

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

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/**
 * @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() генерирует исключение, если метод match matches() (абстрактный метод, который должны реализовать matchers) возвращает false . Метод negativeMatch() работает противоположным образом. Метод matches() для IdentityMatcher использует оператор === для сравнения $subject с аргументом, предоставленным методу matcher:

01
02
03
04
05
06
07
08
09
10
/**
 * @param mixed $subject
 * @param array $arguments
 *
 * @return bool
 */
protected function matches($subject, array $arguments)
{
   return $subject === $arguments[0];
}

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

1
$this->getUser()->shouldNotBeEqualTo($anotherUser);

Который в конечном итоге вызовет negativeMatch() и убедитесь, что matches() возвращает false.

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?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 hinted в наш пример, PhpSpec автоматически использует отражение, чтобы внедрить экземпляр класса, который мы будем использовать. Но StdClass то, что мы уже видели, действительно ли мы верим, что мы действительно получили экземпляр StdClass ? Давайте посмотрим на get_class() еще раз:

1
2
$ vendor/bin/phpspec run
string(28) «PhpSpec\Wrapper\Collaborator»

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

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

1
2
3
4
$user->getAge()->willReturn(10);
 
$this->setUser($user);
$this->getUserStatus()->shouldReturn(‘child’);

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

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

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

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

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

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

Несколько месяцев назад известный член PHP-сообщества Матиас Веррэс опубликовал в Твиттере « Фреймворк модульного тестирования в твиттере ». Задача состояла в том, чтобы объединить исходный код функциональной платформы модульного тестирования в один твит. Как видно из сути, код действительно функционален и позволяет вам писать базовые модульные тесты. Концепция модульного тестирования на самом деле довольно проста: проверьте какое-либо утверждение и уведомите пользователя о результате.

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

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

1
2
3
4
public function testTrue()
{
   $this->assertTrue(false);
}

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

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
// 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 это:

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

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

Счастливы!

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