Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Business logic testing with Behat, UI and API

Business logic testing with Behat, UI and API

Presentation presented on PHPers Summit 2023 conference on 27th of May 2023.

Mateusz Zalewski

May 27, 2023
Tweet

Other Decks in Programming

Transcript

  1. BEHAVIOUR DRIVEN DEVELOPMENT BDD is a process designed to aid

    the management and the delivery of software development projects by improving communication between engineers and business professionals. https://inviqa.com/insights/bdd-guide 2
  2. 3

  3. 4

  4. 5

  5. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 8
  6. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 9
  7. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" @ui 10
  8. Given the store has a product "T-shirt banana" priced at

    "$12.54" /** * @Given /^the store has a product "([^"]+)" priced at ("[^"]+")$/ */ public function storeHasAProductPricedAt( string $productName, int $price = 100 ): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } 11
  9. Given the store has a product "T-shirt banana" priced at

    "$12.54" /** * @Given /^the store has a product "([^"]+)" priced at ("[^"]+")$/ */ public function storeHasAProductPricedAt( string $productName, int $price = 100 ): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } $product = $this->createProduct($productName, $price, $channel); 12
  10. private function createProduct( string $productName, int $price = 100, ChannelInterface

    $channel = null ): ProductInterface { // ... $product = $this->productFactory->createWithVariant(); $product->setCode(StringInflector::nameToUppercaseCode($productName)); $product->setName($productName); // ... $productVariant = $this->defaultVariantResolver->getVariant($product); $productVariant->addChannelPricing( $this->createChannelPricingForChannel($price, $channel) ); // ... return $product; } 13
  11. Given the store has a product "T-shirt banana" priced at

    "$12.54" /** * @Given /^the store has a product "([^"]+)" priced at ("[^"]+")$/ */ public function storeHasAProductPricedAt( string $productName, int $price = 100 ): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } priced at ("[^"]+") int $price = 100 14
  12. /** * @Transform /^"(\-)?(?:€|£|¥|\$)((?:\d+\.)?\d+)"$/ */ public function getPriceFromString(string $sign, string

    $price): int { $this->validatePriceString($price); $price = (int) round((float) $price * 100, 2); if ('-' === $sign) { $price *= -1; } return $price; } 15
  13. When I add this product to the cart /** *

    @When /^I (?:add|added) (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCart(); } @ui 16
  14. When I add this product to the cart /** *

    @When /^I (?:add|added) (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCart(); } (this product) ProductInterface $product 17 @ui
  15. /** * @Transform /^(?:this|that|the) ([^"]+)$/ */ public function getResource(mixed $resource):

    mixed { return $this->sharedStorage->get( StringInflector::nameToCode($resource) ); } 18
  16. private function saveProduct(ProductInterface $product) { $this->productRepository->add($product); $this->sharedStorage->set('product', $product); } public

    function set($key, $resource): void { $this->clipboard[$key] = $resource; $this->latestKey = $key; } 19
  17. private function saveProduct(ProductInterface $product) { $this->productRepository->add($product); $this->sharedStorage->set('product', $product); } /**

    * @Transform /^(?:this|that|the) ([^"]+)$/ */ public function getResource(string $resource): mixed { return $this->sharedStorage->get( StringInflector::nameToCode($resource) ); } /** * @Given /^I (?:add|added) (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCart(); } 20
  18. $this->productShowPage->open(['slug' => $product->getSlug()]); public function open(array $urlParameters = []): void

    { $this->tryToOpen($urlParameters); $this->verify($urlParameters); } public function tryToOpen(array $urlParameters = []): void { $this->getSession()->visit($this->getUrl($urlParameters)); } public function verify(array $urlParameters = []): void { $this->verifyStatusCode(); $this->verifyUrl($urlParameters); } 21 @ui
  19. $this->productShowPage->addToCart(); public function addToCart(): void { $this->getElement('add_to_cart_button')->click(); } protected function

    getDefinedElements(): array { return array_merge(parent::getDefinedElements(), [ 'add_to_cart_button' => '#addToCart button', // ... ]); } 22 @ui
  20. Then there should be one item in my cart /**

    * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart() { Assert::true($this->summaryPage->isSingleItemOnPage()); } @ui 23
  21. $this->summaryPage->isSingleItemOnPage(); public function isSingleItemOnPage(): bool { $items = $this ->getElement('cart_items')

    ->findAll('css', '[data-test-cart-product-row]') ; return 1 === count($items); } 24 @ui
  22. 26

  23. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui @api Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 27
  24. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui @api Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" @api 28
  25. Given the store has a product "T-shirt banana" priced at

    "$12.54" /** * @Given /^the store has a product "([^"]+)" priced at ("[^"]+")$/ */ public function storeHasAProductPricedAt( string $productName, int $price = 100 ): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } 29
  26. When I add this product to the cart /** *

    @When /^I (?:add|added) (this product) to the (cart)$/ */ public function iAddThisProductToTheCart( ProductInterface $product, ?string $tokenValue ): void { $tokenValue = $this->pickupCart(); $request = Request::customItemAction( 'shop', 'orders', $tokenValue, HttpRequest::METHOD_POST, 'items' ); $request->updateContent([ 'productVariant' => // variant identifier, 'quantity' => 1, ]); $this->cartClient->executeCustomRequest($request); $this->sharedStorage->set('product', $product); } 30 @api
  27. When I add this product to the cart /** *

    @When /^I (?:add|added) (this product) to the (cart)$/ */ public function iAddThisProductToTheCart( ProductInterface $product, ?string $tokenValue ): void { $tokenValue = $this->pickupCart(); $request = Request::customItemAction( 'shop', 'orders', $tokenValue, HttpRequest::METHOD_POST, 'items' ); $request->updateContent([ 'productVariant' => // variant identifier, 'quantity' => 1, ]); $this->cartClient->executeCustomRequest($request); $this->sharedStorage->set('product', $product); } $request = Request::customItemAction( 'shop', 'orders', $tokenValue, HttpRequest::METHOD_POST, 'items' ); @api 31
  28. Request::customItemAction public static function customItemAction( ?string $section, string $resource, string

    $id, string $type, string $action ): RequestInterface { return new self( sprintf('/api/v2/%s/%s/%s/%s', $section, $resource, $id, $action), $type, ['CONTENT_TYPE' => self::resolveHttpMethod($type)], ); } 32 @api
  29. <itemOperation name="shop_add_item"> <attribute name="method">POST</attribute> <attribute name="path">/shop/orders/{tokenValue}/items</attribute> <attribute name="messenger">input</attribute> <attribute name=„input">Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart</attribute>

    <attribute name="normalization_context"> <attribute name="groups">shop:cart:read</attribute> </attribute> <attribute name="denormalization_context"> <attribute name="groups">shop:cart:add_item</attribute> </attribute> <attribute name="openapi_context"> <attribute name="summary">Adds Item to cart</attribute> </attribute> </itemOperation> @api 33
  30. Then there should be one item in my cart /**

    * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart(): void { $response = $this->cartsClient->getLastResponse(); $items = $this->responseChecker->getValue($response, 'items'); Assert::count($items, 1); } 35 @api
  31. public function getValue(Response $response, string $key): mixed { $content =

    json_decode($response->getContent(), true); Assert::isArray($content); Assert::keyExists($content, $key); return $content[$key]; } $items = $this->responseChecker->getValue($response, 'items'); @api 36
  32. { "@context": "/api/v2/contexts/Order", "@id": "/api/v2/shop/orders/{orderToken}", "@type": „Order", // ... "items":

    [ { "@id": "/api/v2/shop/order-items/866997", "@type": "OrderItem", "variant": "/api/v2/shop/product-variants/variant1", // ... } ], // ... } 37
  33. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui @api @application Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 39
  34. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui @api @application Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" @application 40
  35. Given the store has a product "T-shirt banana" priced at

    "$12.54" /** * @Given /^the store has a product "([^"]+)" priced at ("[^"]+")$/ */ public function storeHasAProductPricedAt( string $productName, int $price = 100 ): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } 41
  36. When I add this product to the cart @application /**

    * @When /^I add (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $cart = $this->cartContext->getCart(); try { $this ->commandBus ->dispatch(new AddToCart($cart, $product, 1)) ; } catch (HandlerFailedException $exception) { $this ->sharedStorage ->set('last_exception', $exception->getPrevious()) ; } } 42
  37. When I add this product to the cart /** *

    @When /^I add (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $cart = $this->cartContext->getCart(); try { $this ->commandBus ->dispatch(new AddToCart($cart, $product, 1)) ; } catch (HandlerFailedException $exception) { $this ->sharedStorage ->set('last_exception', $exception->getPrevious()) ; } } $this ->sharedStorage ->set('last_exception', $exception->getPrevious()) ; And I should be notified that the product has been successfully added @application 43
  38. final readonly class AddToCart { public function __construct( public OrderInterface

    $cart, public ProductInterface $product, public int $quantity, ) { } } 44 @application
  39. final class AddToCartHandler implements MessageHandlerInterface { public function __construct( private

    FactoryInterface $orderItemFactory, private OrderModifierInterface $orderModifier, private OrderItemQuantityModifierInterface $orderItemQuantityModifier, private ProductVariantResolverInterface $productVariantResolver, private EntityManagerInterface $entityManager, ) { } public function __invoke(AddToCart $command): void { $variant = $this->productVariantResolver->getVariant($command->product); $orderItem = $this->orderItemFactory->createNew(); $orderItem->setVariant($variant); $this->orderItemQuantityModifier->modify($orderItem, $command->quantity); $this->orderModifier->addToOrder($command->cart, $orderItem); $this->entityManager->persist($command->cart); $this->entityManager->flush(); } } @application 45
  40. Then there should be one item in my cart /**

    * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart(): void { $cart = ($this->latestCartQuery)(); Assert::count($cart->getItems(), 1); $cartItem = $cart->getItems()->first(); $this->sharedStorage->set('item', $cartItem); } 46 @application
  41. final class LatestCartQuery { public function __construct(private OrderRepositoryInterface $orderRepository) {

    } public function __invoke(): OrderInterface { return $this->orderRepository->findLatestCart(); } } @application 47
  42. imports: - suites/application/cart/shopping_cart.yaml # ... - suites/api/admin/login.yml - suites/api/cart/accessing_cart.yml -

    suites/api/cart/shopping_cart.yml - suites/api/channel/channels.yml - suites/api/channel/managing_channels.yml # ... - suites/ui/admin/locale.yml - suites/ui/admin/login.yml - suites/ui/cart/shopping_cart.yml - suites/ui/channel/channels.yml # ... 49
  43. default: suites: application_shopping_cart: contexts: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel - sylius.behat.context.transform.currency

    - sylius.behat.context.transform.lexical - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.exchange_rate - sylius.behat.context.setup.product - sylius.behat.context.setup.promotion - sylius.behat.context.setup.shipping - sylius.behat.context.setup.shipping_category - sylius.behat.context.setup.shop_security - sylius.behat.context.setup.taxation - sylius.behat.context.setup.user - sylius.behat.context.setup.zone - Sylius\Behat\Context\Application\CartContext filters: tags: "@shopping_cart && @application" 50
  44. default: suites: application_shopping_cart: contexts: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel - sylius.behat.context.transform.currency

    - sylius.behat.context.transform.lexical - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.exchange_rate - sylius.behat.context.setup.product - sylius.behat.context.setup.promotion - sylius.behat.context.setup.shipping - sylius.behat.context.setup.shipping_category - sylius.behat.context.setup.shop_security - sylius.behat.context.setup.taxation - sylius.behat.context.setup.user - sylius.behat.context.setup.zone - Sylius\Behat\Context\Application\CartContext filters: tags: "@shopping_cart && @application" - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel - sylius.behat.context.transform.currency - sylius.behat.context.transform.lexical - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.exchange_rate - sylius.behat.context.setup.product - sylius.behat.context.setup.promotion - sylius.behat.context.setup.shipping - sylius.behat.context.setup.shipping_category - sylius.behat.context.setup.shop_security - sylius.behat.context.setup.taxation - sylius.behat.context.setup.user - sylius.behat.context.setup.zone - Sylius\Behat\Context\Application\CartContext 51
  45. default: suites: application_shopping_cart: contexts: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel - sylius.behat.context.transform.currency

    - sylius.behat.context.transform.lexical - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.zone - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.exchange_rate - sylius.behat.context.setup.product - sylius.behat.context.setup.promotion - sylius.behat.context.setup.shipping - sylius.behat.context.setup.shipping_category - sylius.behat.context.setup.shop_security - sylius.behat.context.setup.taxation - sylius.behat.context.setup.user - sylius.behat.context.setup.zone - Sylius\Behat\Context\Application\CartContext filters: tags: "@shopping_cart && @application" tags: "@shopping_cart && @application" 52
  46. default: suites: api_v1_shopping_cart: contexts: - Sylius\Behat\Context\Api\V1\CartContext filters: tags: "@shopping_cart &&

    @api && @v1” api_v2_shopping_cart: contexts: - Sylius\Behat\Context\Api\V2\CartContext filters: tags: "@shopping_cart && @api && @v2" 53
  47. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui @api @application @mixed Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 54
  48. When I add this product to the cart /** *

    @Given /^I (?:add|added) (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCart(); } @mixed 55 Then there should be one item in my cart /** * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart(): void { $cart = $this->cartRepository->getLatest(); Assert::count($cart->getItems(), 1); $cartItem = $cart->getItems()->first(); }
  49. When I add this product to the cart /** *

    @Given /^I (?:add|added) (this product) to the cart$/ */ public function iAddProductToTheCart(ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCart(); } @mixed 56 Then there should be one item in my cart /** * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart(): void { $cart = $this->cartRepository->getLatest(); Assert::count($cart->getItems(), 1); $cartItem = $cart->getItems()->first(); } ?
  50. $this->shopClient->executeCustomRequest($request); $this->adminClient->index(Resources::PRODUCT_VARIANTS); $this->client->index(); <service id="sylius.behat.api_platform_client.admin.exchange_rate" class="Sylius\Behat\Client\ApiPlatformClient" parent="sylius.behat.api_platform_client"> <argument>exchange-rates</argument> <argument>admin</argument> </service>

    <service id=„sylius.behat.context.api.admin.managing_exchange_rates" class="Sylius\Behat\Context\Api\Admin\ManagingExchangeRatesContext"> <argument type="service" id="sylius.behat.api_platform_client.admin.exchange_rate" /> ... </service> 58
  51. 59

  52. 60

  53. 61

  54. Background: Given the store operates on a single channel in

    "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 64
  55. Feature: Adding a simple product to the cart In order

    to select products for purchase As a Visitor I want to be able to add simple products to cart Background: Given the store operates on a single channel in "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana" 65
  56. 66 Feature: Adding a simple product to the cart In

    order to start the checkout process with the desired products As a Visitor I want to be able to add simple products to cart Background: Given the store operates on a single channel in "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana"
  57. 67 Feature: Adding a simple product to the cart In

    order to start the process of changing my clothing style As a Visitor I want to be able to add simple products to cart Background: Given the store operates on a single channel in "United States" And the store has a product "T-shirt banana" priced at "$12.54" And the store ships everywhere for free @ui Scenario: Adding a simple product to the cart When I add this product to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added And there should be one item in my cart And this item should have name "T-shirt banana"