Статьи

Тестирование Frenzy – Можем ли мы тестировать BDD?

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

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

Перидот Лого

Перидот

Peridot – это среда тестирования BDD (так что тестирование зависит от поведения ), но для ваших блоков кода, а не для вашей бизнес-логики.

Чего ждать?

Да.

Если вы знакомы с Behat , вы узнаете этот синтаксис (он должен быть достаточно читабельным, даже если вы не знакомы с ним):

Feature: adding a todo As a user I want my todos to be persisted So I don't have to retype them Scenario: adding a todo Given I am on "/" When I fill in "todo" with "Get groceries" And I press "add" And I reload the page Then I should see "Get groceries" Scenario: adding a duplicate todo Given I have a done todo "Pick up dinner" And I am on "/" When I fill in "todo" with "Pick up dinner" And I press "add" Then I should see "Todo already exists" after waiting And I should see 1 "#todos li" elements 

Отдельные фразы определены в классах FeatureContext следующим образом:

  /** * @Then I should see :arg1 after waiting */ public function iShouldSeeAfterWaiting($text) { $this->getSession()->wait(10000, "document.documentElement.innerHTML.indexOf('$text') > -1"); $this->assertPageContainsText($text); } /** * @Given I have a todo :arg1 */ public function iHaveATodo($todoText) { $collection = self::getTodoCollection(); $collection->insert(['label' => $todoText, 'done' => false]); } 

Каркас распознает их, подставляет аргументы для их значений и проверяет условия.

Но они называются «пользовательскими историями» – это то, что пользователь делает или должен делать. Behat буквально откроет браузер при необходимости, проверит и отчитается. Как мы могли бы применить это к единицам кода?

Вот так:

 describe('ArrayObject', function() { beforeEach(function() { $this->arrayObject = new ArrayObject(['one', 'two', 'three']); }); describe('->count()', function() { it('should return the number of items', function() { $count = $this->arrayObject->count(); assert($count === 3, 'expected 3'); }); }); }); 

В ArrayObject стиле на основе истории с каскадным вложенным контекстом Peridot будет «описывать» и ArrayObject count() ArrayObject (обратите внимание, как он сначала описывает ArrayObject, затем описывает метод позже), и добавит it для объяснения того, что описанный объект должен делать.

Вложенный контекстный подход также позволяет вам выполнять некоторую логику начальной загрузки перед созданием каждого объекта или перед созданием каждого субъекта, делая инфраструктуру тестирования невероятно гибкой. Это то, что beforeEach функция beforeEach – она ​​инициирует новый ArrayObject с тремя элементами, поэтому счетчик всегда выполняется на соответствующем экземпляре.

Хорошо, так … что? Это просто альтернативный синтаксис для PHPUnit? Ну да и нет. Несмотря на то, что он ориентирован на модули и способен тестировать их на основе истории, вы также можете рассматривать его как язык посредника между клиентами и разработчиками. Это также делает Behat таким привлекательным – если вы дадите клиенту функцию Behat, он сразу же сможет сказать, что это значит и что он должен тестировать. Немного потренировавшись, они даже смогут написать больше историй для вас – а клиенты, пишущие тесты для своего собственного приложения, являются святым Граалем проекта каждого разработчика, я уверен, что вы согласны.

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

Подробный отчет

совпадение

Peridot также предлагает параллелизм «просто работает» с плагином для параллелизма .

Это очень удобно, когда вы тестируете соединения с базой данных, удаленные вызовы или действия браузера – все это очень медленно, и выполнение их один за другим может затянуться в любой системе. Вместо этого плагин параллелизма обеспечивает выполнение тестов в отдельных рабочих процессах, которые создаются по мере необходимости. После этого каждый работник сообщает результаты родительскому процессу, что значительно ускоряет тестирование. Нет необходимости в настройке, кроме активации плагина и передачи флага ( --concurrent и --processes ) при запуске тестов.

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

 $id = getenv('PERIDOT_TEST_TOKEN'); $dbname = "mydb_$id"; 

Фокус и Пропустить

Чтобы конкурировать с PHPUnit, Peridot также предлагает способ полностью пропустить некоторые тесты и запускать только другие. Если вы добавите к функции префикс f , то тестируемая спецификация станет сфокусированной . Это причудливый способ сказать, что будет выполнен только тест, и все, что не найдено в этом наборе, не будет запущено . Полезно при тонкой настройке теста.

 <?php describe('A suite with nested suites', function() { fcontext('A focused suite', function() { it('should execute this spec', function() { // ... }); it('should also execute this spec', function() { // ... }); }); describe('An unfocused suite', function() { it('should not execute this spec', function() { // ... }); }); }); 

В других случаях вы захотите пропустить тест и пометить его как временно неважный или незавершенный. Когда ты это сделаешь? Например, когда особенно ужасная реализация JSON-расширения «оставляй-заменяй -но-не-действительно- падай, если мы будем честными» для PHP сломала мои тесты Diffbot PHP Client, я мог не делайте ничего другого, но пометьте его как пропущенный и дождитесь расширения, которое действительно сработало. Префикс x в Peridot – это то же самое, что и пометка теста, пропущенного в PHPUnit.

 xdescribe('A pending suite', function() { xcontext('when using a pending context', function() { xit('should have a pending spec', function() { // ... }); }); }); 

Эти две функции в комбинации могут конкурировать с аналогичными функциями других сред тестирования.

События и плагины

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

События можно прослушать в основном файле peridot.php , и их полный список доступен здесь . С такой открытой архитектурой могут быть легко разработаны всевозможные плагины. Плагин Yo , например, отправит Yo! уведомление через службу Yo, когда тесты пройдены или не пройдены (в зависимости от конфигурации yo [sic]). Плагин Watcher будет отслеживать ваши тестовые файлы и повторно запускать пакет, когда будут обнаружены изменения (совместимость зависит от ОС – пожалуйста, используйте не Windows. Homestead Improved было бы лучше).

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

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

Вот как можно интегрировать его с отчетом о покрытии кода PHPUnit.

Custom DSL

Если вам не нравится синтаксис description describe-it и вы хотите воспроизвести что-то более удобное для себя или что-то более подходящее для вашего бизнеса, то в качестве замены или дополнения могут быть разработаны пользовательские DSL (специфичные для домена языки). С помощью пользовательских DSL это становится возможным в Peridot:

 Feature("chdir"," As a PHP user I need to be able to change the current working directory", function () { Scenario(function () { Given('I am in this directory', function () { chdir(__DIR__); }); When('I run getcwd()', function () { $this->cwd = getcwd(); }); Then('I should get this directory', function () { if ($this->cwd != __DIR__) { throw new \Exception("Should be current directory"); } }); }); }); 

Полный пример того, как это сделать, доступен здесь .

Пользовательские области

Также возможно естественное расширение объема теста. Например, если мы хотим иметь возможность использовать веб-драйвер (веб-браузер для тестирования URL-адресов) в наших тестах, мы могли бы активировать его, установив WebDriver следующим образом:

 composer require facebook/webdriver peridot-php/webdriver-manager 

Затем вы создаете класс, расширяющий область действия Peridot, чтобы его можно было привязать к тестам:

 <?php // src/Example/WebDriverScope namespace Peridot\Example; use Evenement\EventEmitter; use Peridot\Core\Scope; class WebDriverScope extends Scope { /** * @var \RemoteWebDriver */ protected $driver; /** * @var \Evenement\EventEmitter */ protected $emitter; /** * @param \RemoteWebDriver $driver */ public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter) { $this->driver = $driver; $this->emitter = $emitter; //when the runner has finished lets quit the driver $this->emitter->on('runner.end', function() { $this->driver->quit(); }); } /** * Add a getPage method to our tests * * @param $url */ public function getPage($url) { $this->driver->get($url); } /** * Adds a findElementById method to our tests * * @param $id * @return \WebDriverElement */ public function findElementById($id) { return $this->driver->findElement(\WebDriverBy::id($id)); } } функции <?php // src/Example/WebDriverScope namespace Peridot\Example; use Evenement\EventEmitter; use Peridot\Core\Scope; class WebDriverScope extends Scope { /** * @var \RemoteWebDriver */ protected $driver; /** * @var \Evenement\EventEmitter */ protected $emitter; /** * @param \RemoteWebDriver $driver */ public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter) { $this->driver = $driver; $this->emitter = $emitter; //when the runner has finished lets quit the driver $this->emitter->on('runner.end', function() { $this->driver->quit(); }); } /** * Add a getPage method to our tests * * @param $url */ public function getPage($url) { $this->driver->get($url); } /** * Adds a findElementById method to our tests * * @param $id * @return \WebDriverElement */ public function findElementById($id) { return $this->driver->findElement(\WebDriverBy::id($id)); } } функции <?php // src/Example/WebDriverScope namespace Peridot\Example; use Evenement\EventEmitter; use Peridot\Core\Scope; class WebDriverScope extends Scope { /** * @var \RemoteWebDriver */ protected $driver; /** * @var \Evenement\EventEmitter */ protected $emitter; /** * @param \RemoteWebDriver $driver */ public function __construct(\RemoteWebDriver $driver, EventEmitter $emitter) { $this->driver = $driver; $this->emitter = $emitter; //when the runner has finished lets quit the driver $this->emitter->on('runner.end', function() { $this->driver->quit(); }); } /** * Add a getPage method to our tests * * @param $url */ public function getPage($url) { $this->driver->get($url); } /** * Adds a findElementById method to our tests * * @param $id * @return \WebDriverElement */ public function findElementById($id) { return $this->driver->findElement(\WebDriverBy::id($id)); } } 

Затем это активируется в основном файле peridot.php :

 <?php // peridot.php use Peridot\Core\Suite; use Peridot\Example\WebDriverScope; require_once __DIR__ . '/vendor/autoload.php'; return function($emitter) { //create a single WebDriverScope to port around $driver = RemoteWebDriver::create('http://localhost:4444/wd/hub', array( WebDriverCapabilityType::BROWSER_NAME => WebDriverBrowserType::FIREFOX )); $webDriverScope = new WebDriverScope($driver, $emitter); /** * We want all suites and their children to have the functionality provided * by WebDriverScope, so we hook into the suite.start event. Suites will pass their child * scopes to all child tests and suites. */ $emitter->on('suite.start', function(Suite $suite) use ($webDriverScope) { $scope = $suite->getScope(); $scope->peridotAddChildScope($webDriverScope); }); }; 

… И неожиданно появившийся в тестах!

 <?php describe('The home page', function() { it('should have a greeting', function() { $this->getPage('http://localhost:4000'); $greeting = $this->findElementById('greeting'); assert($greeting->getText() === "Hello", "should be Hello"); }); }); 

Обратите внимание, как мы можем полностью повторить поведение Бехата с этим!

Больше информации об областях применения здесь .

Вывод

Перидот – это способ оживить ваши модули кода, следуя шагам самого приложения, когда оно проверяется на BDD. Сам по себе это отличный инструмент для модульного тестирования, но в сочетании с чем-то вроде Behat он может довольно хорошо охватить все приложение.

Синтаксис description describe-it немного странный для нас, кто не знаком с другими инструментами, использующими его, но он соответствует контексту, в котором он выполняется (описывая модули), поэтому по смыслу он работает довольно хорошо. Кроме того – можно сделать свой собственный DSL, если describe-it нехорошо.

Включая Peridot, у нас теперь есть четыре основных игрока на рынке тестирования: PHPUnit, Behat, PHPSpec и Peridot.

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

Приведенные выше примеры кода были взяты из примера приложения Peridot