Статьи

Тестирование PHP-кода с помощью Atoum — альтернатива PHPUnit

Эта статья была рецензирована Кристофером Питтом , @jubianchi и Иваном Эндерлином . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Если вы работали с PHP дольше, вы, несомненно, начали тестировать свой код. И если вы спросите кого-либо в пространстве PHP, что использовать для написания модульных тестов, вероятно, первым ответом, который они вам дадут, будет PHPUnit.

Это фактический стандарт в сообществе PHP, и на то есть веские причины. Но это не единственный выбор. В то время как он действительно управляет львиной долей, существует множество других вариантов, один из которых я собираюсь рассказать вам в этом уроке; это называется атум .

Если вы впервые слышите об этом, это называется:

Простая, современная и интуитивно понятная среда модульного тестирования для PHP!

Логотип Atoum

Я не могу обязательно ручаться за то, что он настолько интуитивен, но он определенно прост в использовании. И хотя в его composer.json настоящее время указана минимальная версия PHP 5.3.3, один из разработчиков ядра сказал мне, что с версии 3.0 поддержка PHP 5.3 будет официально прекращена в пользу PHP 5.6. Так что не обманывайте себя, это современный взгляд на тестирование в PHP.

Недавно я решил провести тестовый прогон на существующей моей кодовой базе, приложении для мониторинга работоспособности, которое я писал, чтобы помочь мне справиться с некоторыми проблемами со здоровьем, которые у меня были за последние 12-24 месяца. Мне показалось интересным заняться тестированием программного обеспечения, объединив несколько разных стилей в один пакет.

Установка Atoum

Как и все современное программное обеспечение PHP, мы устанавливаем его с помощью Composer .

 composer require atoum/atoum 

В целях данного руководства я предполагаю, что вы используете PhpStorm. Не обижайтесь на Vim , Emacs и других редакторов.

Учитывая это, и учитывая, как структурирован исходный код atoum, мы также должны установить дополнительную библиотеку под названием atoum / stubs .

Без этого попытка выполнить любой вид автозавершения кода в вашей IDE с помощью atoum — это не просто. Заглушки делают работу с atoum гораздо более удобной для пользователей, что подтверждается на рисунке ниже.

Использование заглушек atoum с PhpStorm для завершения кода

 composer require atoum/stubs 

Конфигурирование Atoum

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

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

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

Чтобы начать настройку atoum, создайте новый файл в корневом каталоге вашего проекта с именем .atoum.php . Там добавьте следующее содержимое:

 <?php use mageekguy\atoum; $stdout = new atoum\writers\std\out; $report = new atoum\reports\realtime\santa; $script->addReport( $report->addWriter($stdout) ); - <?php use mageekguy\atoum; $stdout = new atoum\writers\std\out; $report = new atoum\reports\realtime\santa; $script->addReport( $report->addWriter($stdout) ); 

Нет, вы не мечтаете, что стиль отчетности установлен на Санта. Если вы стонете, что до Рождества еще несколько месяцев, это уже октябрь. Я уверен, что магазины в вашем районе скоро будут заполнены всеми безвкусными рождественскими товарами.

Но прежде чем слишком сильно стонать, есть ряд других вариантов; к ним относятся: cli, phing, tap и NyanCat .

После этого давайте установим покрытие кода для экспорта в формат HTML. Создайте новую структуру каталогов /coverage/html вне корневого каталога вашего проекта.

Затем скопируйте vendor/atoum/resources/configurations/runner/coverage.php.dist в корневую директорию вашего проекта под именем coverage.php . Там нам нужно обновить записи конфигурации, перечисленные во фрагменте кода ниже:

 <?php use \mageekguy\atoum\report\fields\runner\coverage; $coverageHtmlField = new coverage\html( 'Your project name', '/path/to/destination/directory' ); $coverageHtmlField->setRootUrl('http://url/of/web/site'); $coverageTreemapField = new coverage\treemap( 'Your project name', '/path/to/destination/directory' ); $coverageTreemapField ->setTreemapUrl('http://url/of/treemap') ->setHtmlReportBaseUrl($coverageHtmlField->getRootUrl()); $script ->addDefaultReport() ->addField($coverageHtmlField) ->addField($coverageTreemapField); 

После этого мы готовы начать писать тесты.

Наш первый тест

В исходном коде моего приложения у меня есть объект Journal , в котором хранится запись о ежедневном потреблении пищи, которую я пишу для отслеживания того, что я ел, и как я на него отреагировал. В нем у меня есть следующая функция:

 public function populate(array $data) { if (empty($data)) { throw new HydrationException(); } $this->id = $data['id']; $this->userId = $data['userId']; $this->entry = $data['entry']; $this->created = $data['created']; $this->updated = $data['updated']; } 

Вы можете видеть, что он принимает один параметр, $data , который является массивом. Если $data пуст, он HydrationException исключение HydrationException , которое является пользовательским исключением.

Если $data не пустой, то он заполняет каждую из пяти закрытых переменных-членов, используя ключ с тем же именем из $data . Ничего особенного, но достаточно для тестирования.

Для создания первого теста по умолчанию мы не следуем тому же подходу, который вы можете найти во многих других проектах, таких как Zend Framework, Symfony или Laravel.

Из коробки atoum предлагает другой стиль — создание каталога с именем tests/units , относительно классов, которые вы собираетесь тестировать.

Итак, в случае с моей сущностью Journal , которая находится в src/App/Entity , я бы затем создал новый тестовый класс, также называемый Journal , в tests/units/Entity/Journal.php .

Затем я бы дал ему пространство namespace App\Entity\tests\units; типа PSR-4, пространство namespace App\Entity\tests\units; , После этого мне нужно только использовать пространство имен atoum, и я могу начать писать тесты.

 <?php namespace App\Entity\tests\units; use App\Entity\Exception\HydrationException; use App\Entity\Journal as Entity; use Faker\Factory; use Faker\Generator; use atoum; 

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

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

 class Journal extends atoum { public function testHydration() { $faker = Factory::create(); $data = [ 'id' => $faker->randomDigitNotNull, 'userId' => $faker->randomDigitNotNull, 'entry' => $faker->text, 'created' => $faker->dateTime, 'updated' => $faker->dateTime, ]; $this ->given($entity = new Entity()) ->when($entity->populate($data)) ->then ->integer($entity->getId()) ->isEqualTo($data['id']); } } 

У Atoum есть несколько стилей для написания тестов. Вы можете использовать BDD / Gherkin-стиль given -> when -> then , как я уже говорил выше. Или вы можете использовать более прямой подход, который вы можете увидеть в примере ниже.

 faker = Factory::create(); $entity = new Entity(); $data = [ 'id' => faker->randomDigitNotNull, 'userId' => faker->randomDigitNotNull, 'entry' => faker->text, 'created' => faker->dateTime, 'updated' => faker->dateTime, ]; $entity->populate($data); $this ->object($entity) ->integer($entity->getId()) ->isEqualTo($this->data['id']); 

Лично, хотя я немного более многословен, я предпочитаю первый стиль второму. Я нахожу это более читабельным и интуитивно понятным. Выбор ваш.

Теперь давайте посмотрим немного глубже на тесты. Мы использовали given метод для создания преамбулы или для настройки тестируемой системы (SUT), которая является объектом сущности Journal . Затем мы использовали метод when для запуска SUT и получения результата, в частности, вызвав его метод populate populate() чтобы гидрировать его тестовыми данными.

После гидратации объекта мы затем вычислили вердикт теста, используя then() вместе с двумя утверждениями, сравнивая пост-состояние объекта с данными, используемыми для его гидратации. В этой оценке состояния SUT мы проверяем, является ли переменная-член id Journal , возвращенная путем вызова $entity->getId() , целочисленным значением и равна значению в $data ‘ элемент id.

Поскольку существует несколько свойств, которые могут быть установлены для объекта, мы могли бы продолжать использовать свободный интерфейс для проверки их всех, например, в следующем примере:

 $this->object($entity) ->integer($entity->getId())->isEqualTo(data['id']) ->integer($entity->getUserId())->isEqualTo(data['userId']) ->string($entity->getEntry())->isEqualTo(data['entry']) ->dateTime($entity->getCreated())->isEqualTo(data['created']) ->dateTime($entity->getUpdated())->isEqualTo(data['updated']); 

Здесь мы использовали другой метод утверждения, string() , для значений, которые возвращают строку, и dateTime() для значений dateTime вместо integer() . Однако, как и следовало ожидать от всеобъемлющей библиотеки тестирования, существует ряд других вариантов утверждений.

Выполнение тестов

Когда тест готов, самое время его запустить. В командной строке в корневом каталоге вашего проекта выполните следующую команду:

 ./vendor/bin/atoum -c coverage.php -f src/App/Entity/tests/units/Journal.php 

Это запустит только созданный нами тест с использованием ключа -f и включит покрытие кода с помощью ключа -c . Когда вы запустите его, вы должны увидеть результат, подобный показанному на рисунке ниже — хо хо хо !

Запуск атум тестов

Если вы хотите, не стесняйтесь изменить стиль вывода на что-нибудь еще; мой личный фаворит — NyanCat . Кто бы не любил сумасшедший вывод, как это:

Запуск атум тестов с NyanCat

Покрытие кода

Теперь, когда у нас есть рабочий тест, давайте подробнее рассмотрим отчеты о покрытии кода, начиная с вывода командной строки. В приведенном ниже сокращенном примере мы видим, что для протестированного класса App\Entity\Journal общее покрытие кода составляет 93,33%. Это потому, что его __toString вообще не имеет покрытия.

 > Code coverage value: 93.33% => Class App\Entity\Journal: 93.33% ==> App\Entity\Journal::__toString(): 0.00% Success (1 test, 2/2 methods, 0 void method, 0 skipped method, 13 assertions)! > Code coverage value: 93.33% => Class App\Entity\Journal: 93.33% ==> App\Entity\Journal::__toString(): 0.00% > Running duration: 0.15 second. 

Теперь давайте посмотрим на вывод покрытия HTML. php -S 127.0.0.1:8080 -t ./coverage/html веб-сервер PHP с помощью команды php -S 127.0.0.1:8080 -t ./coverage/html , мы можем увидеть HTML-отчет по адресу http://localhost:8080 :

Отчет о покрытии HTML-кода atoum

Там мы видим список всех классов, включенных в отчет о покрытии, и их уровень покрытия кода. Нажав на нее, мы увидим, как оценивается уровень покрытия кода.

Отчет о покрытии HTML-кода atoum

Каждый из методов bar __toString() имеет 100% покрытие кода. Просматривая источник, мы видим, выделены зеленым цветом, что было покрыто, и оранжевым, что еще предстоит.

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

Тесты отладки

Но что, если что-то пойдет не так? Скажем, например, я не установил значение для id в массиве $data или ожидал, что оно будет соответствовать другому значению. Учитывая, что при запуске я ожидал увидеть такую ​​ошибку:

Отладка атум тестов

В этой ситуации не всегда легко увидеть, что происходит не так. Тем не менее, atoum предлагает несколько вариантов в этом случае. Они представлены в форме методов dump() , stop() и executeOnFailure() .

Я сосредоточусь на dump() , но не стесняйтесь взглянуть на остальные два самостоятельно. Dump, подобно PHP var_dump() , будет var_dump() содержимое переменной в stdout , как часть выполнения теста. Чтобы использовать его, мне просто нужно добавить вызов в тесте, например так:

 $this ->dump($entity) ->object($entity) ->integer($entity->getId())->isEqualTo(100000000000) ->integer($entity->getUserId())->isEqualTo($data['userId']) ->string($entity->getEntry())->isEqualTo($data['entry']) ->dateTime($entity->getCreated())->isEqualTo($data['created']) ->dateTime($entity->getUpdated())->isEqualTo($data['updated']) ->dump($entity) 

Однако следует помнить одну вещь: методы отладки будут работать только в том случае, если тесты выполняются в режиме отладки. Чтобы сделать это, мы должны использовать ключи -d или --debug при вызове atoum. Когда вы это сделаете, вы увидите вывод, похожий на следующий.

 => In App\Entity\tests\units\Journal::testSimpleHydration(): /Users/settermjd/Workspace/settermjd/health-monitor/vendor/atoum/atoum/classes/test.php:429: class App\Entity\Journal#195 (5) { private $id => int(1) private $userId => int(3) private $entry => string(165) "Reiciendis quod at voluptatem cupiditate error exercitationem at. Deserunt quos vero omnis est aliquam qui. Esse at quo dolorum fugit. Qui voluptas omnis amet nihil." private $created => class DateTime#194 (3) { public $date => string(26) "1992-06-18 01:55:51.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } private $updated => class DateTime#193 (3) { public $date => string(26) "2016-09-28 08:27:18.000000" public $timezone_type => int(3) public $timezone => string(3) "UTC" } } 

Здесь вы можете увидеть структуру сущности Journal и сравнить, что вы ожидали получить с тем, что было получено.

Использование аннотаций

Тестирование на разных версиях PHP

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

Предположим, вы начинаете реорганизовывать некоторые из существующих библиотек вашего приложения для работы с PHP 7, но они не готовы для введения в основную ветку, поскольку она все еще сильно зависит от PHP 5.6. Atoum может отфильтровывать тесты для тех аспектов кода, которые не могут быть протестированы для данной среды выполнения PHP, с помощью аннотации @php .

Atoum, используя аннотации, позволяет указать действующую версию PHP для запуска теста. Это делается с помощью аннотации @php . Для методов, которые требуют PHP 7, мы указываем @php 7.0 в заголовке блока PhpDoc. Для всех остальных методов мы можем оставить их как есть. Если весь тестовый класс требует PHP 7, мы можем использовать это в аннотации класса вместо того, чтобы использовать его для каждого теста, содержащегося в классе.

Теперь это может показаться странным, что хочется сделать. Естественно, если у вас есть класс, который использует больше чем PHP 7, например подсказки скалярного типа, если вы запускаете сценарий (или приложение) с чем-то отличным от PHP 7, то это приведет к фатальным ошибкам. Поэтому я бы предположил, что эта функция будет использоваться, когда вы работаете через процесс портирования.

расширения

А что если вам нужны специальные расширения для тестового метода? Как и в случае с @php , мы можем указать @extensions . Скажем, нам нужно одно или сочетание расширений mbstring, PDO и Xdebug. Мы могли бы использовать аннотацию @extensions , как @extensions ниже, чтобы потребовать эти расширения.

 /** * @extensions mbstring * Or * @extensions pdo xdebug */ 

Благодаря умелому использованию этих аннотаций мы можем приступить к созданию более сложных тестов, а также более сложной инфраструктуры автоматизации тестирования; тот, который позволяет нам легко перенести наше приложение с PHP 5.6 на 7.0 (или 7.1).

В заключение

Это был общий обзор атума с глубоким погружением в ряд областей. Существует гораздо больше предложений, которые я не смог охватить, такие как расширения.

Существует [расширение видимости]) (https://github.com/atoum/visibility-extension), которое позволяет тестировать защищенные и частные методы. Существует расширение линейки, которое позволяет фильтровать выполняемые тесты. Затем есть расширение Blackfire , которое позволяет вам писать тестовые наборы Blackfire, используя atoum.

Кроме того, существует интеграция с CI-сервером , поскольку atoum может быть интегрирован, среди прочего, с Hudson, Jenkins и TravisCI.

Но не позволяйте этому введению быть всем, что вы открываете. Хотя atoum новее, чем библиотека фактического тестирования PHP, PHPUnit, я думаю, что стоит задуматься над этой библиотекой — особенно, если PHPUnit работает не совсем так, как вам нравится.

Имейте в виду, что в этом проекте сильное сообщество, несколько менеджеров по выпуску и основная команда разработчиков. В этом много чего, и начать не так интуитивно, как мне бы хотелось. Однако, как только вы начнете работать, это действительно может вырасти на вас!

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

Что ты думаешь об атуме? Вы дали ему попробовать? Вы будете?

  • https://github.com/atoum/bdd-extension
  • https://github.com/atoum/visibility-extension
  • https://github.com/atoum/json-schema-extension
  • https://github.com/atoum/ruler-extension
  • https://github.com/atoum/atoum