Статьи

Обновление Sylius TDD Way: Изучение Behat

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

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

Просматривая список продуктов, мы хотим видеть новый столбец с именем Inventory котором будет храниться сумма всех доступных запасов отслеженных вариантов.


Логотип Sylius

Написание StoryBDD тестов

Вот какой инструмент мы хотим использовать здесь. Убедившись, что Behat работает хорошо, запустив любую функцию из пакета Sylius, мы создаем новый файл features/product/managing_products/browsing_products_with_inventory.feature со следующим определением:

 @managing_inventory Feature: Browsing products with inventory In order to manage my shop merchandise As an Administrator I want to be able to browse products Background: Given the store operates on a single channel in "United States" And the store has a product "Kubus" And it comes in the following variations: | name | price | | Kubus Banana | $2.00 | | Kubus Carrot | $2.00 | And there are 3 units of "Kubus Banana" variant of product "Kubus" available in the inventory And there are 5 units of "Kubus Carrot" variant of product "Kubus" available in the inventory And I am logged in as an administrator @ui Scenario: Browsing defined products with inventory Given the "Kubus Banana" product variant is tracked by the inventory And the "Kubus Carrot" product variant is tracked by the inventory When I want to browse products Then I should see that the product "Kubus" has 8 on hand quantity 

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

 php bin/behat features/product/managing_products/browsing_products_with_inventory.feature 
 --- Use --snippets-for CLI option to generate snippets for following ui_managing_inventory suite steps: When I want to browse products Then I should see that the product "Kubus" has 18 on hand quantity 

Мы создаем наш контекст в src/Sylius/Behat/Context/Ui/Admin/ManagingProductsInventoryContext.php и добавляем это:

 <?php // src/Sylius/Behat/Context/Ui/Admin/ManagingProductsInventoryContext.php namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; class ManagingProductsInventoryContext implements Context { } 

Повторное выполнение функции, похоже, не помогает, поскольку мы получаем тот же список контекстов, что и раньше. Это потому, что Силиус ничего не знает о нашем классе. Нам нужно настроить сервис для нашего контекста вместе с тем, что Sylius имеет в src/Sylius/Behat/Resources/config/services/contexts/ui.xml . Теперь мы ищем managing_products и добавляем это ниже:

 <service id="sylius.behat.context.ui.admin.managing_products_inventory" class="Sylius\Behat\Context\Ui\Admin\ManagingProductsInventoryContext"> <argument type="service" id="sylius.behat.page.admin.product.index" /> <tag name="fob.context_service" /> </service> 

Давайте добавим наш сервис sylius.behat.context.ui.admin.managing_products_inventory (это id в ui.xml ) в контекстные сервисы для ui_managing_inventory в src/Sylius/Behat/Resources/config/suites/ui/inventory/managing_inventory.yml

Возможно, нам нужно очистить кеш. Если мы запустим эту функцию, мы получим возможность выбрать Sylius\Behat\Context\Ui\Admin\ManagingProductsInventoryContext . Затем мы получаем:

 --- Sylius\Behat\Context\Ui\Admin\ManagingProductsInventoryContext has missing steps. Define them with these snippets: /** * @When I want to browse products */ public function iWantToBrowseProducts() { throw new PendingException(); } /** * @Then I should see that the product :arg1 has :arg2 on hand quantity */ public function iShouldSeeThatTheProductHasOnHandQuantity($arg1, $arg2) { throw new PendingException(); } 

Мы можем просто скопировать и вставить фрагменты в созданный нами контекстный класс. Из любопытства мы можем импортировать PendingException чтобы увидеть результат. Давайте добавим use Behat\Behat\Tester\Exception\PendingException; к вершине класса и перезапустите функцию.

Мы получаем ошибку:

 An exception occured in driver: SQLSTATE[HY000] [1049] Unknown database 'xxxx_test' (Doctrine\DBAL\Exception\ConnectionException) 

Это потому, что мы не создали тестовую базу данных. Эти две команды сделают это для нас сейчас.

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

Если вы создали тестовую базу данных до изменения таблицы product_variant для столбца reorder_level в предыдущем посте, возможно, вы получаете сообщение об ошибке:

 Column not found: 1054 Unknown column 'reorder_level' in 'field list' 

Затем давайте обновим тестовую базу данных:

 php bin/console doctrine:schema:update --env=test --force 

Функцию теперь можно запустить, и вы можете увидеть это в середине вывода:

 When I want to browse products TODO: write pending definition 

Видимый ТОДО в выводе Behat

«TODO: определение в ожидании записи» — это сообщение PendingException. Возвращаясь к нашему ManagingProductsInventoryContext , чего-то еще не хватает. У настроенной нами службы был аргумент: <argument type="service" id="sylius.behat.page.admin.product.index" /> ; но это еще не в нашем классе.

Служба sylius.behat.page.admin.product.index предназначена для посещения sylius.behat.page.admin.product.index страницы управления продуктами. Если мы посмотрим в src/Sylius/Behat/Resources/config/services/pages/admin/product.xml (найденный путем поиска, где определено sylius.behat.page.admin.product.index ), мы увидим, что класс — Sylius\Behat\Page\Admin\Product\IndexPage . Нам нужно внедрить интерфейс в наш контекстный класс.

Также в iWantToBrowseProducts() мы теперь можем посетить страницу индекса, вызвав правильный метод из нашего экземпляра IndexPageInterface . Поэтому наш ManagingProductsInventoryContext.php должен выглядеть следующим образом:

 <?php // src/Sylius/Behat/Context/Ui/Admin/ManagingProductsInventoryContext.php use Behat\Behat\Context\Context; use Sylius\Behat\Page\Admin\Product\IndexPageInterface; use Behat\Behat\Tester\Exception\PendingException; class ManagingProductsInventoryContext implements Context { /** * @var IndexPageInterface */ private $indexPage; /** * @param IndexPageInterface $indexPage */ public function __construct(IndexPageInterface $indexPage) { $this->indexPage = $indexPage; } /** * @When I want to browse products */ public function iWantToBrowseProducts() { $this->indexPage->open(); } /** * @Then I should see that the product :arg1 has :arg2 on hand quantity */ public function iShouldSeeThatTheProductHasOnHandQuantity($arg1, $arg2) { throw new PendingException(); } } 

Это проходит, и мы получаем подсказку для другого отсутствующего определения:

 Then I should see that the product "Kubus" has 18 on hand quantity TODO: write pending definition 

Сначала мы хотим изменить имена аргументов, чтобы мы знали, к чему они относятся — $product и $quantity . Type-hinting $product также хорошая идея. У Sylius есть много общих методов, которые мы можем вызвать, чтобы проверить, что у нас есть столбец с именем «инвентарь» и у нас есть правильное количество доступных предметов для нашего тестируемого продукта. Вот как должен выглядеть метод iShouldSeeThatTheProductHasOnHandQuantity() :

 /** * @Then I should see that the product :product has :quantity on hand quantity */ public function iShouldSeeThatTheProductHasOnHandQuantity(Product $product, $quantity) { Assert::notNull($this->indexPage->getColumnFields('inventory')); Assert::true($this->indexPage->isSingleResourceOnPage([ 'name' => $product->getName(), 'inventory' => sprintf('%d Available on hand', $quantity), ])); } 

Мы представили два класса: Assert и Product . Давайте импортируем их с:

 use Webmozart\Assert\Assert; use AppBundle\Entity\Product; 

В то же время вы можете удалить импорт PendingException который больше не используется.

Теперь сценарий завершается с другим сообщением об ошибке:

 Then I should see that the product "Kubus" has 18 on hand quantity Column with name "inventory" not found! (InvalidArgumentException) 

Behat открыл страницу со списком товаров, и там нет колонки Inventory . Давайте добавим это. Списки обычно отображаются с помощью компонента Sylius Grid. SyliusGridBundle использует файлы конфигурации YAML, которые описывают структуру данной сетки. Для административного раздела они расположены в src/Sylius/Bundle/AdminBundle/Resources/config/grids/ . Нам нужно только скопировать то, что находится в product_variant.yml для нашего столбца «инвентарь» в product.yml .

 inventory: type: twig path: . label: sylius.ui.inventory options: template: "@SyliusAdmin/ProductVariant/Grid/Field/inventory.html.twig" 

Самое главное, мы должны правильно переопределить product.yml . Мы копируем src/Sylius/Bundle/AdminBundle/Resources/config/grids/product.yml в app/Resources/SyliusAdminBundle/config/grids/product.yml . Мы добавляем конфигурацию колонки инвентаря выше к ней, возможно после «имени». После очистки кешей и запуска тестов все должно пройти.

Теперь мы хотим изменить inventory.html.twig с помощью нашей логики уровня переупорядочения, чтобы мы могли указывать, когда запас становится низким. Это позаботится и ProductVariant сетках Product и о ProductVariant поскольку их поле инвентаризации использует один и тот же шаблон. Давайте скопируем src/Sylius/Bundle/AdminBundle/Resources/views/ProductVariant/Grid/Field/inventory.html.twig в app/Resources/SyliusAdminBundle/views/ProductVariant/Grid/Field/inventory.html.twig и заменим содержимое на :

 {% if data.isTracked %} {% if data.onHand > 0 %} {% set classes = (data.isReorderable) ? 'yellow' : 'green' %} {% else %} {% set classes = 'red' %} {% endif %} <div class="ui {{ classes }} icon label"> <i class="cube icon"></i> <span class="onHand" data-product-variant-id="{{ data.id }}">{{ data.onHand }}</span> {{ 'sylius.ui.available_on_hand'|trans }} {% if data.onHold > 0 %} <div class="detail"> <span class="onHold" data-product-variant-id="{{ data.id }}">{{ data.onHold }}</span> {{ 'sylius.ui.reserved'|trans }} </div> {% endif %} </div> {% else %} <span class="ui red label"> <i class="remove icon"></i> {{ 'sylius.ui.not_tracked'|trans }} </span> {% endif %} 

После того, как мы очистим кеш, мы сможем просмотреть список из интерфейса администратора. Давайте отредактируем некоторые варианты продукта, изменим уровень запасов и просмотрим ожидаемые результаты. Если в каком-либо из вариантов на складе имеется 5 или менее отслеживаемых товаров, будет третий цвет (желтый), указывающий, что запас находится на уровне заказа для этого товара. Все в порядке, за исключением того, что у нас нет возможности изменить уровень заказа. Мы обсудим это с тем, как настроить форму варианта продукта, чтобы мы могли ее изменить. Здесь нужно запомнить 3 важных слова — класс, сервис и конфигурация.

Настройка формы ProductVariant

Мы настраиваем форму, расширяя класс формы. Symfony использует сервисный контейнер для стандартизации того, как объекты создаются в приложении. Если бы только мы могли видеть список услуг … возможно, мы могли бы найти некоторую информацию о том, с чем можно работать? Давайте попробуем это:

 php bin/console debug:container product_variant 

Вы получите список сервисных идентификаторов и, пройдя их, вы увидите один с «sylius.form.type.product_variant», который выглядит так, как нам нужно. Выберите это и обратите внимание, что класс, который нам нужно расширить, это Sylius\Bundle\ProductBundle\Form\Type\ProductVariantType .

Создать сервис

Давайте создадим src/AppBundle/Resources/config/services.yml и добавим это:

 services: app.form.extension.type.product_variant: class: AppBundle\Form\Type\Extension\ProductVariantTypeExtension tags: - { name: form.type_extension, extended_type: Sylius\Bundle\ProductBundle\Form\Type\ProductVariantType } 

Создать класс

Услуга, определенная выше, предназначена для класса. Давайте создадим src/AppBundle/Form/Type/Extension/ProductVariantTypeExtension.php и добавим следующее:

 <?php // src/AppBundle/Form/Type/Extension/ProductVariantTypeExtension.php namespace AppBundle\Form\Type\Extension; use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantType; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; class ProductVariantTypeExtension extends AbstractTypeExtension { /** * {@inheritdoc} */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add( 'reorderLevel', TextType::class, [ 'required' => false, 'label' => 'sylius.form.variant.reorder_level', ] ); } /** * {@inheritdoc} */ public function getExtendedType() { return ProductVariantType::class; } } 

Метка требует файл конфигурации сообщений. Мы создадим app/Resources/SyliusAdminBundle/translations/messages.en.yml и добавим следующее:

 sylius: form: variant: reorder_level: 'Reorder level' 

Мы видим иерархию ключей в имени — sylius.form.variant.reorder_level . Давайте проинформируем Sylius о сервисе в app/config/config.yml , добавив его в разделе app/config/config.yml :

 - { resource: "@AppBundle/Resources/config/services.yml"} 

Переопределение шаблона

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

Слева находится код статуса HTTP, а рядом с ним имя маршрута. При нажатии на название маршрута отображается профилировщик. Существует много данных о Запросе, среди которых есть раздел Атрибуты запроса со следующими ключами: _controller , _route , _route_params и id . Столбец Значение для _sylius показывает стрелку, указывающую массив. Нажатие на него расширяет массив ключами «section», «template», «redirect», «Права» и «vars». Значением «шаблона» является SyliusAdminBundle:Crud:update.html.twig который находится по адресу src/Sylius/Bundle/AdminBundle/Resources/views/Crud/update.html.twig .

Если посмотреть на update.html.twig , среди них есть один из них: {% include '@SyliusAdmin/Crud/Update/_content.html.twig' %} . Опять же, это можно найти в src/Sylius/Bundle/AdminBundle/Resources/views/Crud/Update/_content.html.twig . Там нет ничего интересного в файле. Тупик!

Если мы вернемся к атрибутам запроса и _vars а затем ключ templates , мы увидим @SyliusAdmin/ProductVariant/_form.html.twig который мы находим в src/Sylius/Bundle/AdminBundle/Resources/views/ProductVariant/_form.html.twig . В _form.html.twig есть функция Twig knp_menu_get() . Меню? Какое меню? Мы ищем форму. Это другой тип страницы, созданный с использованием разных шаблонов. Оглядываясь на саму страницу, мы видим две вкладки — «Детали» и «Налоги». Вкладки и меню объединяются, так что, возможно, мы на правильном пути.

Возвращаясь к списку сервисов, которые мы видели в php bin/console debug:container product_variant , мы можем найти один из них с «menu» и «product_variant». Действительно, есть один: sylius.admin.menu_builder.product_variant_form который при отладке дает нам некоторую интересную информацию:

Отладочная форма построителя меню

Для параметра «Теги» используется значение knp_menu.menu_builder (method: createMenu, alias: sylius.admin.product_variant_form) . Этот псевдоним является первым параметром метода knp_menu_get() в нашем шаблоне _form.html.twig . Мы определенно на правильном пути.

Из приведенной выше информации об отладке мы также знаем, что Sylius\Bundle\AdminBundle\Menu\ProductVariantFormMenuBuilder отвечает за форму. Открыв класс и посмотрев на метод createMenu() мы увидим наши вкладки details и taxes с соответствующими шаблонами — @SyliusAdmin/ProductVariant/Tab/_details.html.twig и @SyliusAdmin/ProductVariant/Tab/_taxes.html.twig . Мы можем подтвердить, что нашли правильный шаблон для переопределения, проверив src/Sylius/Bundle/AdminBundle/Resources/views/ProductVariant/Tab/_details.html.twig .

Давайте скопируем файл в app/Resources/SyliusAdminBundle/views/ProductVariant/Tab/_details.html.twig . Найдя раздел инвентаря, мы добавляем новое поле {{ form_row(form.reorderLevel) }} любом месте. Я положил свой между {{ form_row(form.onHand) }} и {{ form_row(form.tracked) }} . Если мы вернемся к форме варианта продукта, текстовое поле должно быть прямо здесь. Мы можем изменить значение по умолчанию на любое число и сохранить файл.

Вывод

Заключительная часть нашей серии завершена — мы смогли добавить новые функциональные возможности в Sylius, не неся при этом долговременных технических долгов и не жертвуя 100% тестовым покрытием приложения. Мы рассмотрели все виды тестирования, доступные в этой среде электронной коммерции, и максимально использовали ее лучшие практики.

В частности, в этой части мы рассмотрели написание тестов SpecBDD более подробно и как переписать модели и формы Sylius. Будут разные и более интересные способы достижения одинаковых целей. Дайте нам знать ваше мнение о некоторых из них.

Вы уже используете Sylius? Почему, почему нет? Какие подводные камни вы видите в наших подходах? Давайте обсудим!