Статьи

Силиус и режущий зубы на TDD

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


Логотип Sylius

Sylius — это приложение для электронной коммерции, основанное на Symfony . Он имеет 100% покрытие кода , что впечатляет для PHP-приложений такого размера. В этой статье мы рассмотрим различные виды доступных тестов и попробуем некоторые разработки, основанные на тестировании и поведении (TDD / BDD). Инструкции см. На странице руководства по установке Sylius , так как в этой статье предполагается, что у вас есть работающая установка с примерами данных, и вы можете запускать тесты Behat , PHPUnit и phpspec .

В корне сети есть папка src которая содержит весь код, связанный с Sylius. Это позволяет вам использовать папку app для разработки вашего приложения без ненужного наступления на пальцы Sylius. Поскольку мы заинтересованы в разработке на основе тестов (во-первых, писать тесты, которые не выполняются до написания кода), давайте углубимся в путь Sylius.

Мы начнем с настройки нашей тестовой базы данных.

 php bin/console doctrine:database:create --env=test php bin/console doctrine:schema:create --env=test 

Типы тестов Sylius

Некоторые из основ нижеприведенных инструментов уже были рассмотрены в этом посте , но мы напомним их здесь на примерах Sylius, чтобы соответствовать теме.

PHPUnit

Sylius поставляется с большим количеством функциональных тестов PHPUnit . Файл конфигурации, phpunit.xml.dist , находится в корневом веб- phpunit.xml.dist , а модульные тесты находятся в папке tests . Из корня нашего приложения давайте запустим тесты в tests/Controller/CountryApiTest.php :

 ./vendor/phpunit/phpunit/phpunit -c ./phpunit.xml.dist tests/Controller/CountryApiTest 

PHPUnit в действии

Команда состоит из 3 частей — пути к PHPUnit, нашего файла конфигурации и класса модульного тестирования. Откройте tests/Controller/CountryApiTest.php и взгляните на класс. Он расширяет JsonApiTestCase, который мы можем проследить до ApiTestCase и WebTestCase, части пакета фреймворка Symfony.

Phpspec

Behavior Driven Development (BDD) возникла из Test Driven Development (TDD), сосредоточив внимание на том, как пользователь взаимодействует с приложением и как ведет себя приложение. SpecBDD является частью BDD, которая рассматривает, как меньшие биты приложения работают вместе, чтобы все происходило.

Sylius устанавливается с phpspec который является инструментом, необходимым для этого. В вашем корневом каталоге также phpspec.yml.dist файл конфигурации phpspec.yml.dist . Спецификации написаны в классах PHP и могут быть сгруппированы в комплекты.

Помните, Sylius — это большое приложение, поэтому там много файлов. Откройте src/Sylius/Component/Order/spec/Model/OrderItemSpec.php .

Первое, на что следует обратить внимание, это то, что независимо от того, насколько глубокой является структура папки, у вас есть спецификации в папке spec и исходный код, к которому применяются тесты, легко найти. Если вы посмотрите на уровень папки spec , вы увидите Model внутри которой находится класс OrderItem . Спецификация для этого класса — spec/Model/OrderItemSpec.php . Сравните функции, и вы увидите, как они связаны.

Когда вы запускаете phpspec, вы получаете больше выходных данных с опцией --verbose а с -fpretty вы можете получить более симпатичные выходные данные.

 ./bin/phpspec run -fpretty --verbose src/Sylius/Component/Order/spec/Model/OrderItemSpec.php 

PHPSpec в действии в Sylius

Behat

StoryBDD — это вторая сторона монеты BDD, которая представляет поведение в повествовании, фокусируя внимание на более широкой картине того, что должно происходить в приложении. Behat является основным инструментом, который делает возможным StoryBDD. Вы запускаете Behat, он анализирует специальные файлы, называемые функциями, и проверяет, соответствует ли поведение вашего сайта описанию в этих файлах. Другие инструменты необходимы для имитации или эмуляции функциональности браузеров. Норка — это библиотека, которая нужна Бехату для этого, в то время как что-то вроде Laravel будет использовать превосходные закаты .

Описание поведения, которое вы пишете, находится в формате, называемом огурчик . Каждое поведение называется сценарием , и один файл объектов может иметь несколько сценариев. Файлы должны иметь расширение feature .

Sylius поставляется с behat.yml.dist конфигурации behat.yml.dist и папкой features с подпапками, в которых организованы файлы функций. Файл конфигурации и папка функций должны находиться на одном уровне. Откройте features/order/managing_orders/browsing_orders.feature и посмотрите на структуру.

Давайте дадим этой функции тест-драйв:

 ./bin/behat features/order/managing_orders/browsing_orders.feature 

Функция тестируется

См. BDD в Laravel: Начало работы с Behat и PhpSpec для другой замечательной статьи на эту тему.

Давайте сделаем TDD

Мы собираемся изменить страницу со списком заказов в тестовом режиме. Войдите в раздел admin/login , admin/login . Значок в левом верхнем углу выглядит идеально, но давайте попробуем изменить его на что-то другое. Sylius использует Semantic UI для своего интерфейса. Посмотрите на набор иконок . Допустим, мы хотим заменить «магазин» на значок «корзина». Просматривая источник страницы списка заказов, мы хотим перейти от <i class="circular shop icon"></i> к <i class="circular shopping basket icon"></i> .

Шаг 1: Добавить функцию

Мы не начинаем с пустой страницы, поэтому мы идем по пути Sylius. Функции, связанные с заказами, находятся в файле features\order\managing_orders , поэтому мы создаем там файл и называем его browsing_orders_with_visual_display.feature и добавляем:

 @viewing_page_icon Feature: Browsing orders page with icon In order to identify orders page As an Administrator I want to be able to see an icon on the orders page Background: Given the store operates on a single channel in "United States" And the store has a product "PHP T-Shirt" And the store ships everywhere for free And the store allows paying with "Cash on Delivery" And there is a customer "[email protected]" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator @ui Scenario: Seeing an icon on the page When I browse orders Then the "shopping basket" icon should be visible on the page 

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

Шаг 2: Добавить объект страницы

Sylius позволяет нам создавать новые объекты страниц, определенные в контейнере Behat, в etc\behat\services\pages.xml . Мы собираемся создать интерфейс и реализацию в src\Sylius\Behat\Page\Admin\Order\ .

 <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <parameters> <parameter key="sylius.behat.page.admin.order.order_page.class">Sylius\Behat\Page\Admin\Order\OrderPage </parameter> </parameters> <services> <service id="sylius.behat.page.admin.order.order_page" class="%sylius.behat.page.admin.order.order_page.class%" parent="sylius.behat.page.admin.order.index" public="false"> </service> </services> </container> 

Мы информируем сервисный контейнер OrderPage о нашем классе OrderPage . Давайте создадим src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php :

 <?php // src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php namespace Sylius\Behat\Page\Admin\Order; use Sylius\Behat\Page\SymfonyPageInterface; use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; interface OrderPageInterface extends IndexPageInterface { /** * Match icons by class. * * @param string $class * The class to match icons against. * * @return mixed * The matched icons or false. */ public function findIcons($class); } 

Затем, src/Sylius/Behat/Page/Admin/Order/OrderPage.php :

 <?php // src/Sylius/Behat/Page/Admin/Order/OrderPage.php namespace Sylius\Behat\Page\Admin\Order; use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Session; use Sylius\Behat\Page\SymfonyPage; use Sylius\Behat\Service\Accessor\TableAccessorInterface; use Symfony\Component\Routing\RouterInterface; use Sylius\Behat\Page\Admin\Crud\IndexPage; class OrderPage extends IndexPage implements OrderPageInterface { /** * @inheritDoc */ public function findIcons($class) { $foundIcons = $this->getElement('icon')->find('css', '.' . implode('.', explode(' ', $class))); if (!$foundIcons) { throw new ElementNotFoundException($this->getSession(), 'Icons with class(es) ' . $class); } return $foundIcons; } /** * @inheritDoc */ protected function getDefinedElements() { return array_merge(parent::getDefinedElements(), [ 'icon' => '.icon', ]); } } 

Sylius определил более 120 элементов (классов и идентификаторов) для идентификации различных вещей на страницах, но, видимо, не значков. (Поиск src\Sylius\Behat для них). Нам нужно найти класс icon поэтому мы определяем наш собственный в getDefinedElements() . Наш findIcons() ищет класс, который мы передаем из нашей функции. Когда значки имеют более одного имени, например, «корзина покупок», нам нужно найти оба имени.

Шаг 3. Добавьте контекст для запуска тестов.

Контекст — это класс, который также предоставляется как сервис. Начиная с src/Sylius/Behat/Resources/config/services.xml вы найдете <import resource="services/contexts.xml" /> в верхней части. После последующего импорта вы окажетесь в src/Sylius/Behat/Resources/config/services/contexts/ где были настроены некоторые категории контекста ( cli.xml , domain.xml , hook.xml , setup.xml , transform.xml и ui.xml ). Мы за ui.xml . Открой это. Найдите сервис sylius.behat.context.ui.admin.managing_orders и мы добавляем наш прямо под ним:

 <service id="sylius.behat.context.ui.admin.viewing_page_icon" class="Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext"> <argument type="service" id="sylius.behat.page.admin.order.order_page" /> <tag name="fob.context_service" /> </service> 

Обратите внимание на связь между id сервиса и class . Точки заменяются обратными слешами и верблюжьими именами классов.

Создайте src\Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext.php и добавьте следующее:

 <?php // src/Sylius/Behat/Context/Ui/Admin/ViewingPageIconContext.php namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\Page\Admin\Order\OrderPageInterface; use Webmozart\Assert\Assert; use Behat\Behat\Tester\Exception\PendingException; class ViewingPageIconContext implements Context { /** * @var OrderPageInterface */ private $orderPage; /** * @param OrderPageInterface $orderPage */ public function __construct(OrderPageInterface $orderPage) { $this->orderPage = $orderPage; } /** * @Then /^the "([^"]*)" icon should be visible on the page$/ */ public function theIconShouldBeVisibleOnThePage($class) { $icons = $this->orderPage->findIcons($class); Assert::eq(count($icons), 1, sprintf('Icon with class, %s, not found', $class)); } } 

Мы OrderPageInterface наш OrderPageInterface в этот класс, чтобы, когда Behat нашел наш сценарий, мы смогли вызвать правильный метод из внедренного класса. Мы ожидаем только одну иконку, и мы утверждаем это.

Давайте запустим наш тест. Сбой, потому что еще нет значка корзины покупок. Теперь мы можем пойти дальше и внести изменения. Откройте src/Sylius/Bundle/AdminBundle/Resources/config/routing/order.yml и измените icon: cart на icon: shopping basket . Запустите тест, и он должен быть зеленым.

Проходящий значок теста

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

  • Обновите наши функции файла features\order\managing_orders\browsing_orders_with_visual_display.feature с помощью этого:

     @ui Scenario: Checking row of a cancelled order When I view the summary of the order "#00000022" And I cancel this order When I browse orders Then this order should have order payment state "Cancelled" And the row should be marked with the order payment state "Cancelled" 
  • Мы также обновим OrderPageInterface и OrderPage :

     // src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php /** * Match table rows by class. * * @param string $class * The class to match table rows against. * * @return mixed * The matched rows or false. */ public function findTableRows($class); 
     // src/Sylius/Behat/Page/Admin/Order/OrderPage.php /** * @inheritDoc */ public function findTableRows($class) { $foundRows = $this->getElement('table')->find('css', '.' . $class); if (!$foundRows) { throw new ElementNotFoundException($this->getSession(), 'Rows with class ' . $class); } return $foundRows; } 

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

  • Переопределите столбцы «State» и «Channel» в сетке sylius_admin_order , определенной в src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml .

    • Создайте app\config\grids.yml и добавьте это:
      sylius_grid: grids: sylius_admin_order: fields: channel: enabled: false state: enabled: false 
    • Расскажите Силиусу об этом файле, импортировав его в app\config\config.yml :
      - { resource: "grids.yml" } 
  • Скопируйте src\Sylius\Bundle\UiBundle\Resources\views\Macro\table.html.twig в app\Resources\SyliusUiBundle\views\Macro\table.html.twig и в {% macro row(grid, definition, row) %} , замените <tr class="item"> на <tr class="item {{ row.state | lower }}"> . Все, что нам нужно сделать, это добавить CSS для выходных классов здесь.

  • Скопируйте src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\PaymentState\awaiting_payment.html.twig в app\Resources\SyliusAdminBundle\views\Order\Label\PaymentState\awaiting_payment.html.twig . Мы хотим, чтобы значок и метка были заглавными. Измените файл на этот:

     <span class="ui olive label"> {{ value|trans|upper }} </span> 
  • Скопируйте src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\PaymentState\cancelled.html.twig в app\Resources\SyliusAdminBundle\views\Order\Label\PaymentState\cancelled.html.twig . Измените файл на этот:

     <span class="ui yellow label"> {{ value|trans|upper }} </span> 
  • Скопируйте src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\ShippingState\cancelled.html.twig в app\Resources\SyliusAdminBundle\views\Order\Label\ShippingState\cancelled.html.twig . Мы делаем то же, что и выше:

     <span class="ui yellow label"> {{ value|trans|upper }} </span> 
  • Скопируйте src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\ShippingState\ready.html.twig в app\Resources\SyliusAdminBundle\views\Order\Label\ShippingState\ready.html.twig . Мы делаем то же, что и выше:

     <span class="ui blue label"> {{ value|trans|upper }} </span> 
  • Скопируйте src\Sylius\Bundle\UiBundle\Resources\views\_stylesheets.html.twig в app\Resources\SyliusUiBundle\views\_stylesheets.html.twig . Здесь мы добавляем наш пользовательский CSS-файл чуть ниже стандартного: <link rel="stylesheet" href="{{ asset('assets/admin/css/style.css') }}"> . Есть и другие способы сделать это, но это самый быстрый способ сохранить наши изменения изолированно.

  • Добавьте assets/admin/css/style.css с нашим великолепным CSS.

     .order-row.new { background-color: #c5effd; } .order-row.cancelled { background-color: #fbe7af; } 

Запустите тест снова. Это все зеленое.

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

Наконец, давайте добавим тест, чтобы подтвердить, что столбца Channel больше нет.

  1. Обновите наши функции файла features\order\managing_orders\browsing_orders_with_visual_display.feature с помощью этого:

     Scenario: Not seeing a column on the page When I browse orders Then the "channel" column should not be visible on the page 
  2. Обновите OrderPageInterface.php с помощью:

     // src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php /** * Get table headers. * * @return mixed * The array of headers keys or empty array. */ public function findTableHeaders(); 
  3. OrderPage.php новый метод в OrderPage.php :

     // src/Sylius/Behat/Page/Admin/Order/OrderPage.php /** * {@inheritdoc} */ public function findTableHeaders() { $headers = $this->getTableAccessor() ->getSortableHeaders($this->getElement('table')); return is_array($headers) ? array_keys($headers) : array(); } 

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

  4. Обновите наш контекст ViewingPageIconContext.php :

      // src\Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext.php /** * @Then /^the "([^"]*)" column should not be visible on the page$/ */ public function theColumnShouldNotBeVisibleOnThePage($column) { $foundHeaders = $this->orderPage->findTableHeaders(); Assert::false(in_array(strtolower($column), $foundHeaders), sprintf('%s column not found', $column)); } 

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

Проходящий тест Behat

Вывод

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

Мы изучили StoryBDD, написав пару тестов Behat для демонстрации этого процесса. Хотя это простые примеры, те же принципы применяются при выполнении более сложных задач или работе с тестами SpecBDD или PHPUnit.

Какой подход вы используете при тестировании? Только TDD? Только BDD? И то и другое? Может быть, BDDed TDD ? Дайте нам знать!