Статьи

Обновление Sylius the TDD Way: изучение PhpSpec

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

Пост о разработке и тестировании новых функций Sylius был введением в три типа тестов, которые используются в Sylius — PHPUnit , Phpspec и Behat .

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


Логотип Sylius

У Sylius есть отличное решение для управления запасами . Тем не менее, всегда есть место для настройки или два. Если вы посмотрите на список продуктов (admin / products), там нет информации о наличии товара. Рассматривая варианты продукта, мы видим данные инвентаризации, отслеживаются ли они или нет, а также общее количество товаров на складе, если отслеживаются. Было бы неплохо увидеть такую ​​информацию и на странице с перечнем продуктов. Кроме того, уровень запасов — «все или ничего» — например, зеленая метка гласит «10 Доступно в наличии» или красная «0 Доступно в наличии». Как насчет чего-то промежуточного, скажем, желтой метки «3 Доступно в наличии», чтобы указать, что запас низкий? Затем администратор магазина может решить, что пора пополнить.

Расширить ProductVariant и Модели продуктов

Мы хотим расширить поведение моделей ProductVariant и Product предоставляемых Sylius, чтобы мы могли видеть дополнительную информацию о наличии на складе при просмотре продуктов.

Создать пакет

Сначала мы создаем файл src/AppBundle/AppBundle.php и регистрируем его в app/AppKernel.php .

 <?php // src/AppBundle/AppBundle.php namespace AppBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class AppBundle extends Bundle { } 
 <?php // app/AppKernel.php public function registerBundles() { $bundles = [ // ... new AppBundle\AppBundle(), ]; } 

Далее мы информируем автозагрузчик о новом пакете, добавляя его в раздел «автозагрузка» composer.json .

 { // ... "autoload": { "psr-4": { // ... "AppBundle\\": "src/AppBundle" } } // ... } 

Затем мы запускаем composer dump-autoload для повторного генерирования информации автозагрузчика. Теперь мы можем начать писать некоторые тесты SpecBDD.

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

Наши тесты будут сосредоточены на том, как наши классы Product и ProductVariant помогают нам достичь целей, которые мы поставили для реализации функций, которые мы описали выше. Phpspec должен знать о наших тестах. Мы открываем phpspec.yml.dist и добавляем следующее в «suites»:

 AppBundle: { namespace: AppBundle\Entity, psr4_prefix: AppBundle\Entity, spec_path: src/AppBundle/Entity, src_path: src/AppBundle/Entity } 

Возможно, было бы хорошо очистить кеш сейчас: php bin/console cache:clear . Следующее, что нужно сделать, — это иметь спецификации, в которых мы пишем ожидаемое поведение наших классов, и Phpspec может помочь нам с этим с самого начала. Команда description является отправной точкой и создает класс спецификации:

 php bin/phpspec desc AppBundle/Entity/ProductVariant 

Результат:

 Specification for AppBundle\Entity\ProductVariant created in src/AppBundle/Entity/spec/ProductVariantSpec.php 

Давайте запустим это.

 php bin/phpspec run src/AppBundle/Entity/spec/ProductVariantSpec.php 

Класс ProductVariant , описанный в спецификации, не существует, но Phpspec предложит создать его для нас. После того, как он был создан, мы можем повторить два шага выше и получить наш класс Product .

 php bin/phpspec desc AppBundle/Entity/Product php bin/phpspec run src/AppBundle/Entity/spec/ProductSpec.php 

У Sylius уже есть эти два класса, поэтому нам нужно расширить их в нашем AppBundle. Поскольку Sylius часто использует интерфейсы, мы тоже создаем их.

 <?php // src/AppBundle/Entity/ProductInterface.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\ProductInterface as BaseProductInterface; interface ProductInterface extends BaseProductInterface { } 
 <?php // src/AppBundle/Entity/ProductVariantInterface.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\ProductVariantInterface as BaseProductVariantInterface; interface ProductVariantInterface extends BaseProductVariantInterface { } 
 <?php // src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\Product as BaseProduct; use AppBundle\Entity\ProductInterface; class Product extends BaseProduct implements ProductInterface { } 

Давайте обновим сгенерированный класс ProductVariant с $reorderLevel свойства $reorderLevel мы хотим представить.

 <?php // src/AppBundle/Entity/ProductVariant.php namespace AppBundle\Entity; use AppBundle\Entity\ProductVariantInterface as ProductVariantInterface; use Sylius\Component\Core\Model\ProductVariant as BaseProductVariant; class ProductVariant extends BaseProductVariant implements ProductVariantInterface { const REORDER_LEVEL = 5; /** * @var int */ private $reorderLevel; } 

Переопределение классов Sylius

Давайте проинформируем Sylius о наших классах, чтобы они использовались вместо тех, которые предоставляются Sylius. Мы добавляем следующее в app/config/config.yml :

 sylius_product: resources: product: classes: model: AppBundle\Entity\Product product_variant: classes: model: AppBundle\Entity\ProductVariant 

Мы добавили свойство $reorderLevel умолчанию 5 в класс ProductVariant . Так как это будет отличаться от варианта к варианту, нам нужно изменить таблицу product_variant чтобы мы могли также хранить эту информацию. Поскольку Sylius использует Doctrine ORM, это путь. Чтобы расширить существующее определение таблицы, давайте создадим src/AppBundle/Resources/config/doctrine/ProductVariant.orm.yml и добавим следующее:

 AppBundle\Entity\ProductVariant: type: entity table: sylius_product_variant fields: reorderLevel: column: reorder_level type: integer nullable: true 
           

Ключом для поля является имя свойства модели. Мы должны указать имя столбца, в противном случае Doctrine по умолчанию использует имя свойства «reorderLevel». Если свойство является одним словом, например, «уровень», то столбец может быть исключен из этой конфигурации.

Далее, давайте создадим src/AppBundle/Resources/config/doctrine/Product.orm.yml и добавим это:

 AppBundle\Entity\Product: type: entity table: sylius_product участия AppBundle\Entity\Product: type: entity table: sylius_product 

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

Обновление базы данных

Давайте запустим следующее:

 php bin/console doctrine:migrations:diff 

Эта команда генерирует класс миграции в app/migrations . Обычно это последний, так как имя включает метку времени. Он содержит операторы SQL для изменения таблицы product_variant . В реальных проектах сгенерированного кода может быть недостаточно, но вы можете отредактировать файл в соответствии с вашими требованиями. Для миграции:

 php bin/console doctrine:migrations:migrate 

Если по какой-то причине это не помогло, попробуйте следующее:

 php bin/console doctrine:schema:update --force 

Написание большего количества тестов SpecBDD

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

 php bin/phpspec run -fpretty --verbose src/AppBundle/Entity/spec/ProductVariantSpec.php 
 <?php // src/AppBundle/Entity/spec/ProductVariantSpec.php namespace spec\AppBundle\Entity; use AppBundle\Entity\ProductVariant; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class ProductVariantSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(ProductVariant::class); } } 

$this представляет специальный объект, который будет вести себя точно так же, как наш класс ProductVariant . Однако он будет иметь только те свойства и методы, которые мы указали в примерах. Давайте добавим метод в класс spec.

 function it_has_reorder_level_by_default() { $this->getReorderLevel()->shouldReturn(ProductVariant::REORDER_LEVEL); } 

Мы бы хотели, чтобы наш ProductVariant по ProductVariant имел значение уровня переупорядочения 5. Помните, что наш класс ProductVariant по-прежнему пуст. Давайте попробуем запустить спецификацию. Тест не проходит хорошо, с подсказкой:

 Do you want me to create `AppBundle\Entity\ProductVariant::getReorderLevel()` for you? 

Запуск Phpspec и получение подтверждения для автоматического создания метода

Если мы подтвердим, метод getReorderLevel() в классе. Ожидается, что getReorderLevel() найдет getReorderLevel() который возвращает целое число, представляющее порог запасов, ниже которого запас должен быть пополнен. Все, что нам нужно сделать в методе, это вернуть свойство $reorderLevel .

Обычно return $this->reorderLevel; было бы достаточно, но у нас уже есть продукты в нашем магазине, поэтому нам нужно найти способ получить значение уровня повторного заказа по умолчанию. Один из способов сделать это:

 /** * @return int */ public function getReorderLevel() { return !empty($this->reorderLevel) ? $this->reorderLevel : self::REORDER_LEVEL; } 

Тест проходит. Мы можем обновить ProductVariantInterface с помощью public function getReorderLevel(); , Далее мы хотим убедиться, что можем установить уровень переупорядочения. Вот пример:

 function it_has_reorder_level() { $this->setReorderLevel(10); $this->getReorderLevel()->shouldReturn(10); } 

Когда мы запустим его, Phpspec узнает, что метода setReorderLevel() и предложит его создать. Обновите созданный метод следующим образом:

 /** * @param int $reorderLevel */ public function setReorderLevel($reorderLevel) { $this->reorderLevel = $reorderLevel; } 

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

 function it_is_reorderable_when_tracked_and_available_stock_is_at_reorder_level() { $this->setTracked(true); $this->setOnHand(3); $this->setReorderLevel(4); $this->isReorderable()->shouldReturn(true); } 

Когда мы запускаем спецификацию, он находит все методы, кроме isReorderable() которые он предлагает создать для нас в классе ProductVariant .

Еще одно предложение для создания метода

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

 public function isReorderable() { return $this->isTracked() && ($this->getOnHand() <= $this->getReorderLevel()); } 

Все тесты проходят. Давайте обновим интерфейс с помощью public function isReorderable(); , Возможно, вы также захотите добавить тест, когда доступный запас превышает уровень повторного заказа: он не должен переупорядочиваться.

 function it_is_not_reorderable_when_tracked_and_available_stock_is_greater_than_reorder_level() { $this->setTracked(true); $this->setOnHand(10); $this->setReorderLevel(4); $this->isReorderable()->shouldReturn(false); } 

Запуск спецификации теперь должен дать нам что-то вроде этого:

Пять проходных тестов

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

В Phpspec мы можем написать метод let() который letGo() перед каждым примером, и letGo() после его запуска. В src/AppBundle/Entity/spec/ProductSpec.php мы добавим следующее, не забывая импортировать ProductVariantInterface с use AppBundle\Entity\ProductVariantInterface as VariantInterface; на вершине класса.

 function let(VariantInterface $firstVariant, VariantInterface $secondVariant) { $firstVariant->setOnHand(4); $firstVariant->getOnHand()->willReturn(4); $firstVariant->setReorderLevel(4); $firstVariant->getReorderLevel()->willReturn(4); $firstVariant->isTracked()->willReturn(true); $firstVariant->isReorderable()->willReturn(true); $firstVariant->setProduct($this)->shouldBeCalled(); $this->addVariant($firstVariant); $secondVariant->setOnHand(10); $secondVariant->getOnHand()->willReturn(10); $secondVariant->setReorderLevel(3); $secondVariant->getReorderLevel()->willReturn(3); $secondVariant->isTracked()->willReturn(true); $secondVariant->isReorderable()->willReturn(false); $secondVariant->setProduct($this)->shouldBeCalled(); $this->addVariant($secondVariant); } 

Теперь мы можем приступить к написанию примеров и проверке, чтобы наши тесты были зелеными на каждом этапе пути:

 php bin/phpspec run -fpretty --verbose src/AppBundle/Entity/spec/ProductSpec.php 

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

 function it_gets_products_on_hand_as_sum_of_product_variants_on_hand() { $this->getVariants()->shouldHaveCount(2); $this->getOnHand()->shouldReturn(14); } 

Конечно, когда вы запускаете его, класс Product не имеет getOnHand() поэтому мы просим Phpspec создать его для нас, прежде чем мы его реализуем.

 public function getOnHand() { $onHand = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked()) { $onHand += $variant->getOnHand(); } } return $this->onHand = $onHand; } 

Мы не должны забывать добавить public function getOnHand(); в ProductInterface , тоже.

Наконец, продукт переупорядочен, когда по крайней мере один из его вариантов переупорядочен. Пример выглядит следующим образом:

 function it_recognizes_when_to_reorder_stock() { $this->isReorderable()->shouldReturn(true); } 

Опять же, мы позволяем Phpsepc создать метод isReorderable() . Давайте обновим ProductInterface с помощью public function isReorderable(); , Полные реализации должны быть:

 public function isReorderable() { $variants = $this->getVariants(); $trackedVariants = $variants->filter(function (ProductVariantInterface $variant) { return $variant->isReorderable() === true; }); return $this->reorderable = count($trackedVariants) > 0; } 

Мы также должны иметь возможность проверить, отслеживается ли продукт по аналогичной логике — по крайней мере, один из его вариантов должен быть отслеживаемым. Пример Phpspec для этого прост, поскольку оба варианта, которые мы получили, отслеживаются.

 function it_recognizes_product_as_tracked_if_at_least_one_variant_is_tracked() { $this->isTracked()->shouldReturn(true); } 

Теперь мы запустим спецификацию и обновим сгенерированный isTracked() :

 public function isTracked() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isTracked() === true; } ); return $this->tracked = count($trackedVariants) > 0; } 

Все хорошо. Возможно, нам следует добавить пример, чтобы убедиться, что isTracked() возвращает false если ни один из вариантов не отслеживается. Оба варианта в нашем методе let() уже отслеживаются. Однако, если мы передадим две переменные с соответствующими именами, как в методе let() , мы можем переопределить поведение объектов в нашем новом примере.

 function it_recognizes_product_as_not_tracked_if_no_variant_is_tracked( VariantInterface $firstVariant, VariantInterface $secondVariant ) { $firstVariant->isTracked()->willReturn(false); $secondVariant->isTracked()->willReturn(false); $this->isTracked()->shouldReturn(false); } 

Мы также можем немного поиграть с логическими значениями. Например, если мы изменим $this->isTracked()->shouldReturn(false); $this->isTracked()->shouldReturn(true); мы получаем сообщение как:

 AppBundle/Entity/Product recognizes product as not tracked if no variant is tracked expected true, but got false. 

Давайте изменим его обратно на false , поэтому мы продолжаем со всех зеленых. Это завершает наши тесты для классов Product и ProductVariant . Позже нам понадобится несколько вещей, таких как getOnHold() который проверяет аналогичный метод на наличие вариантов. Мы должны получить класс следующим образом:

 <?php // src/AppBundle/Entity/Product.php namespace AppBundle\Entity; use Sylius\Component\Core\Model\Product as BaseProduct; use AppBundle\Entity\ProductVariantInterface as ProductVariantInterface; class Product extends BaseProduct implements ProductInterface { /** * @var int */ protected $onHold = 0; /** * @var int */ protected $onHand = 0; /** * @var bool */ protected $tracked = false; /** * @var bool */ protected $reorderable = false; public function isTracked() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isTracked() === true; } ); return $this->tracked = count($trackedVariants) > 0; } public function getOnHand() { $onHand = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked() === true) { $onHand += $variant->getOnHand(); } } return $this->onHand = $onHand; } public function getOnHold() { $onHold = 0; foreach ($this->getVariants() as $variant) { if ($variant->isTracked()) { $onHold += $variant->getOnHold(); } } return $this->onHold = $onHold; } public function isReorderable() { $variants = $this->getVariants(); $trackedVariants = $variants->filter( function (ProductVariantInterface $variant) { return $variant->isReorderable() === true; } ); return $this->reorderable = count($trackedVariants) > 0; } } 

Вывод

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

В заключительной части этой серии «Расширение Sylius» мы будем работать с Behat и протестировать визуальный вывод изменений, вызванных нашими функциями. Будьте на связи!