SlideShare a Scribd company logo
WITH BEHAT, TWIG AND API PLATFORM
BUSINESS LOGIC TESTING
@mpzalewski
Zales0123
MATEUSZ ZALEWSKI
@mpzalewski
Zales0123
mpzalewski.com
2
3
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
4
5
6
GOALS
7
Substitutability
Reusability
Fun
8
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
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"
10
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
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);
}
12
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);
13
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;
}
14
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
15
/**
* @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;
}
16
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();
}
@ui
17
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();
}
@ui
(this product)
ProductInterface $product
18
/**
* @Transform /^(?:this|that|the) ([^"]+)$/
*/
public function getResource(mixed $resource): mixed
{
return $this->sharedStorage->get(
StringInflector::nameToCode($resource)
);
}
19
private function saveProduct(ProductInterface $product)
{
$this->productRepository->add($product);
$this->sharedStorage->set('product', $product);
}
20
public function set($key, $resource): void
{
$this->clipboard[$key] = $resource;
$this->latestKey = $key;
}
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();
}
21
$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);
}
@ui
22
$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',
// ...
]);
}
@ui
23
Then there should be one item in my cart
@ui
/**
* @Then there should be one item in my cart
*/
public function thereShouldBeOneItemInMyCart()
{
Assert::true($this->summaryPage->isSingleItemOnPage());
}
24
@ui
$this->summaryPage->isSingleItemOnPage();
public function isSingleItemOnPage(): bool
{
$items = $this
->getElement('cart_items')
->findAll('css', '[data-test-cart-product-row]')
;
return 1 === count($items);
}
25
SCENARIO CONTEXT PAGE
UI
Substitutability
26
27
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"
28
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
29
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);
}
30
When I add this product to the cart
@api
/**
* @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->cartsClient->executeCustomRequest($request);
$this->sharedStorage->set('product', $product);
}
31
When I add this product to the cart
@api
/**
* @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->cartsClient->executeCustomRequest($request);
$this->sharedStorage->set('product', $product);
}
32
$request = Request::customItemAction(
'shop', 'orders', $tokenValue, HttpRequest::METHOD_POST, 'items'
);
Request::customItemAction
@api
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)],
);
}
33
<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">SyliusBundleApiBundleCommandCartAddItemToCart</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
34
POST /api/v2/shop/orders/HcgfktTi8E/items HTTP/2
host: demo.sylius.com
accept: application/json
content-type: application/json
content-length: 90
{
"productVariant": "/api/v2/shop/product-variants/variant1",
"quantity": 3
}
35
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);
}
@api
36
public function getValue(Response $response, string $key): mixed
{
$content = json_decode($response->getContent(), true);
Assert::isArray($content);
Assert::keyExists($content, $key);
return $content[$key];
}
@api
$items = $this->responseChecker->getValue($response, 'items');
37
{
"@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",
// ...
}
],
// ...
}
38
SCENARIO
UI CONTEXT PAGE
UI
API CONTEXT CLIENT
Reusability
39
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"
40
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
41
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);
}
42
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())
;
}
}
43
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())
;
}
}
44
$this
->sharedStorage
->set('last_exception', $exception->getPrevious())
;
And I should be notified that the product has been successfully added
@application
final readonly class AddToCart
{
public function __construct(
public OrderInterface $cart,
public ProductInterface $product,
public int $quantity,
) {
}
}
45
@application
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();
}
}
46
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);
}
@application
47
48
final class LatestCartQuery
{
public function __construct(private OrderRepositoryInterface $orderRepository)
{
}
public function __invoke(): OrderInterface
{
return $this->orderRepository->findLatestCart();
}
}
@application
49
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
# ...
50
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
- SyliusBehatContextApplicationCartContext
filters:
tags: "@shopping_cart && @application"
51
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
- SyliusBehatContextApplicationCartContext
filters:
tags: "@shopping_cart && @application"
52
- 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
- SyliusBehatContextApplicationCartContext
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
- SyliusBehatContextApplicationCartContext
filters:
tags: "@shopping_cart && @application"
tags: "@shopping_cart && @application"
53
default:
suites:
api_v1_shopping_cart:
contexts:
- SyliusBehatContextApiV1CartContext
filters:
tags: "@shopping_cart && @api && @v1”
api_v2_shopping_cart:
contexts:
- SyliusBehatContextApiV2CartContext
filters:
tags: "@shopping_cart && @api && @v2"
54
? Fun
55
$this->shopClient->executeCustomRequest($request);
$this->adminClient->index(Resources::PRODUCT_VARIANTS);
$this->client->index();
<service id="sylius.behat.api_platform_client.admin.exchange_rate"
class="SyliusBehatClientApiPlatformClient" 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="SyliusBehatContextApiAdminManagingExchangeRatesContext">
<argument type="service" id="sylius.behat.api_platform_client.admin.exchange_rate" />
...
</service>
56
57
58
59
Substitutability
Reusability
Fun
60
THANK YOU
QR CODE
@CommerceWeavers
@Sylius QR CODE
@mpzalewski Zales0123

More Related Content

Similar to Confoo 2023 - Business logic testing with Behat, Twig and Api Platform

Write a program that mimics the operations of several vending machin.pdf
Write a program that mimics the operations of several vending machin.pdfWrite a program that mimics the operations of several vending machin.pdf
Write a program that mimics the operations of several vending machin.pdf
eyebolloptics
 
I finished most of the program, but having trouble with some key fea.pdf
I finished most of the program, but having trouble with some key fea.pdfI finished most of the program, but having trouble with some key fea.pdf
I finished most of the program, but having trouble with some key fea.pdf
hardjasonoco14599
 

Similar to Confoo 2023 - Business logic testing with Behat, Twig and Api Platform (20)

TYPO3 ViewHelper Workshop
TYPO3 ViewHelper WorkshopTYPO3 ViewHelper Workshop
TYPO3 ViewHelper Workshop
 
Zero to SOLID
Zero to SOLIDZero to SOLID
Zero to SOLID
 
Symfony World - Symfony components and design patterns
Symfony World - Symfony components and design patternsSymfony World - Symfony components and design patterns
Symfony World - Symfony components and design patterns
 
APIs for catalogs
APIs for catalogsAPIs for catalogs
APIs for catalogs
 
Write a program that mimics the operations of several vending machin.pdf
Write a program that mimics the operations of several vending machin.pdfWrite a program that mimics the operations of several vending machin.pdf
Write a program that mimics the operations of several vending machin.pdf
 
[PHPCon 2023] Blaski i cienie BDD
[PHPCon 2023] Blaski i cienie BDD[PHPCon 2023] Blaski i cienie BDD
[PHPCon 2023] Blaski i cienie BDD
 
Optimizing Magento by Preloading Data
Optimizing Magento by Preloading DataOptimizing Magento by Preloading Data
Optimizing Magento by Preloading Data
 
laravel tricks in 50minutes
laravel tricks in 50minuteslaravel tricks in 50minutes
laravel tricks in 50minutes
 
50 Laravel Tricks in 50 Minutes
50 Laravel Tricks in 50 Minutes50 Laravel Tricks in 50 Minutes
50 Laravel Tricks in 50 Minutes
 
Getting started with ExtBase
Getting started with ExtBaseGetting started with ExtBase
Getting started with ExtBase
 
CodingSerbia2014-JavaVSPig
CodingSerbia2014-JavaVSPigCodingSerbia2014-JavaVSPig
CodingSerbia2014-JavaVSPig
 
Wordpress plugin development from Scratch
Wordpress plugin development from ScratchWordpress plugin development from Scratch
Wordpress plugin development from Scratch
 
Practical Event Sourcing
Practical Event SourcingPractical Event Sourcing
Practical Event Sourcing
 
Angular 2 introduction
Angular 2 introductionAngular 2 introduction
Angular 2 introduction
 
Technology and Science News - ABC News
Technology and Science News - ABC NewsTechnology and Science News - ABC News
Technology and Science News - ABC News
 
Practica n° 7
Practica n° 7Practica n° 7
Practica n° 7
 
Symfony day 2016
Symfony day 2016Symfony day 2016
Symfony day 2016
 
I finished most of the program, but having trouble with some key fea.pdf
I finished most of the program, but having trouble with some key fea.pdfI finished most of the program, but having trouble with some key fea.pdf
I finished most of the program, but having trouble with some key fea.pdf
 
Creating web api and consuming part 2
Creating web api and consuming part 2Creating web api and consuming part 2
Creating web api and consuming part 2
 
Oop php 5
Oop php 5Oop php 5
Oop php 5
 

Recently uploaded

Mastering Windows 7 A Comprehensive Guide for Power Users .pdf
Mastering Windows 7 A Comprehensive Guide for Power Users .pdfMastering Windows 7 A Comprehensive Guide for Power Users .pdf
Mastering Windows 7 A Comprehensive Guide for Power Users .pdf
mbmh111980
 
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
Alluxio, Inc.
 

Recently uploaded (20)

KLARNA - Language Models and Knowledge Graphs: A Systems Approach
KLARNA -  Language Models and Knowledge Graphs: A Systems ApproachKLARNA -  Language Models and Knowledge Graphs: A Systems Approach
KLARNA - Language Models and Knowledge Graphs: A Systems Approach
 
5 Reasons Driving Warehouse Management Systems Demand
5 Reasons Driving Warehouse Management Systems Demand5 Reasons Driving Warehouse Management Systems Demand
5 Reasons Driving Warehouse Management Systems Demand
 
StrimziCon 2024 - Transition to Apache Kafka on Kubernetes with Strimzi
StrimziCon 2024 - Transition to Apache Kafka on Kubernetes with StrimziStrimziCon 2024 - Transition to Apache Kafka on Kubernetes with Strimzi
StrimziCon 2024 - Transition to Apache Kafka on Kubernetes with Strimzi
 
A Comprehensive Appium Guide for Hybrid App Automation Testing.pdf
A Comprehensive Appium Guide for Hybrid App Automation Testing.pdfA Comprehensive Appium Guide for Hybrid App Automation Testing.pdf
A Comprehensive Appium Guide for Hybrid App Automation Testing.pdf
 
How to install and activate eGrabber JobGrabber
How to install and activate eGrabber JobGrabberHow to install and activate eGrabber JobGrabber
How to install and activate eGrabber JobGrabber
 
TROUBLESHOOTING 9 TYPES OF OUTOFMEMORYERROR
TROUBLESHOOTING 9 TYPES OF OUTOFMEMORYERRORTROUBLESHOOTING 9 TYPES OF OUTOFMEMORYERROR
TROUBLESHOOTING 9 TYPES OF OUTOFMEMORYERROR
 
Secure Software Ecosystem Teqnation 2024
Secure Software Ecosystem Teqnation 2024Secure Software Ecosystem Teqnation 2024
Secure Software Ecosystem Teqnation 2024
 
Top Mobile App Development Companies 2024
Top Mobile App Development Companies 2024Top Mobile App Development Companies 2024
Top Mobile App Development Companies 2024
 
IT Software Development Resume, Vaibhav jha 2024
IT Software Development Resume, Vaibhav jha 2024IT Software Development Resume, Vaibhav jha 2024
IT Software Development Resume, Vaibhav jha 2024
 
INGKA DIGITAL: Linked Metadata by Design
INGKA DIGITAL: Linked Metadata by DesignINGKA DIGITAL: Linked Metadata by Design
INGKA DIGITAL: Linked Metadata by Design
 
AI/ML Infra Meetup | Perspective on Deep Learning Framework
AI/ML Infra Meetup | Perspective on Deep Learning FrameworkAI/ML Infra Meetup | Perspective on Deep Learning Framework
AI/ML Infra Meetup | Perspective on Deep Learning Framework
 
Mastering Windows 7 A Comprehensive Guide for Power Users .pdf
Mastering Windows 7 A Comprehensive Guide for Power Users .pdfMastering Windows 7 A Comprehensive Guide for Power Users .pdf
Mastering Windows 7 A Comprehensive Guide for Power Users .pdf
 
Abortion ^Clinic ^%[+971588192166''] Abortion Pill Al Ain (?@?) Abortion Pill...
Abortion ^Clinic ^%[+971588192166''] Abortion Pill Al Ain (?@?) Abortion Pill...Abortion ^Clinic ^%[+971588192166''] Abortion Pill Al Ain (?@?) Abortion Pill...
Abortion ^Clinic ^%[+971588192166''] Abortion Pill Al Ain (?@?) Abortion Pill...
 
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product UpdatesGraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
GraphSummit Stockholm - Neo4j - Knowledge Graphs and Product Updates
 
Studiovity film pre-production and screenwriting software
Studiovity film pre-production and screenwriting softwareStudiovity film pre-production and screenwriting software
Studiovity film pre-production and screenwriting software
 
Microsoft 365 Copilot; An AI tool changing the world of work _PDF.pdf
Microsoft 365 Copilot; An AI tool changing the world of work _PDF.pdfMicrosoft 365 Copilot; An AI tool changing the world of work _PDF.pdf
Microsoft 365 Copilot; An AI tool changing the world of work _PDF.pdf
 
Facemoji Keyboard released its 2023 State of Emoji report, outlining the most...
Facemoji Keyboard released its 2023 State of Emoji report, outlining the most...Facemoji Keyboard released its 2023 State of Emoji report, outlining the most...
Facemoji Keyboard released its 2023 State of Emoji report, outlining the most...
 
10 Essential Software Testing Tools You Need to Know About.pdf
10 Essential Software Testing Tools You Need to Know About.pdf10 Essential Software Testing Tools You Need to Know About.pdf
10 Essential Software Testing Tools You Need to Know About.pdf
 
Crafting the Perfect Measurement Sheet with PLM Integration
Crafting the Perfect Measurement Sheet with PLM IntegrationCrafting the Perfect Measurement Sheet with PLM Integration
Crafting the Perfect Measurement Sheet with PLM Integration
 
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
AI/ML Infra Meetup | Improve Speed and GPU Utilization for Model Training & S...
 

Confoo 2023 - Business logic testing with Behat, Twig and Api Platform

  • 1. WITH BEHAT, TWIG AND API PLATFORM BUSINESS LOGIC TESTING @mpzalewski Zales0123 MATEUSZ ZALEWSKI
  • 3. 3
  • 4. 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 4
  • 5. 5
  • 6. 6
  • 9. 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
  • 10. 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" 10
  • 11. 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 11
  • 12. 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); } 12
  • 13. 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); 13
  • 14. 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; } 14
  • 15. 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 15
  • 16. /** * @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; } 16
  • 17. 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(); } @ui 17
  • 18. 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(); } @ui (this product) ProductInterface $product 18
  • 19. /** * @Transform /^(?:this|that|the) ([^"]+)$/ */ public function getResource(mixed $resource): mixed { return $this->sharedStorage->get( StringInflector::nameToCode($resource) ); } 19
  • 20. private function saveProduct(ProductInterface $product) { $this->productRepository->add($product); $this->sharedStorage->set('product', $product); } 20 public function set($key, $resource): void { $this->clipboard[$key] = $resource; $this->latestKey = $key; }
  • 21. 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(); } 21
  • 22. $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); } @ui 22
  • 23. $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', // ... ]); } @ui 23
  • 24. Then there should be one item in my cart @ui /** * @Then there should be one item in my cart */ public function thereShouldBeOneItemInMyCart() { Assert::true($this->summaryPage->isSingleItemOnPage()); } 24
  • 25. @ui $this->summaryPage->isSingleItemOnPage(); public function isSingleItemOnPage(): bool { $items = $this ->getElement('cart_items') ->findAll('css', '[data-test-cart-product-row]') ; return 1 === count($items); } 25
  • 27. 27
  • 28. 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" 28
  • 29. 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 29
  • 30. 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); } 30
  • 31. When I add this product to the cart @api /** * @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->cartsClient->executeCustomRequest($request); $this->sharedStorage->set('product', $product); } 31
  • 32. When I add this product to the cart @api /** * @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->cartsClient->executeCustomRequest($request); $this->sharedStorage->set('product', $product); } 32 $request = Request::customItemAction( 'shop', 'orders', $tokenValue, HttpRequest::METHOD_POST, 'items' );
  • 33. Request::customItemAction @api 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)], ); } 33
  • 34. <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">SyliusBundleApiBundleCommandCartAddItemToCart</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 34
  • 35. POST /api/v2/shop/orders/HcgfktTi8E/items HTTP/2 host: demo.sylius.com accept: application/json content-type: application/json content-length: 90 { "productVariant": "/api/v2/shop/product-variants/variant1", "quantity": 3 } 35
  • 36. 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); } @api 36
  • 37. public function getValue(Response $response, string $key): mixed { $content = json_decode($response->getContent(), true); Assert::isArray($content); Assert::keyExists($content, $key); return $content[$key]; } @api $items = $this->responseChecker->getValue($response, 'items'); 37
  • 38. { "@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", // ... } ], // ... } 38
  • 39. SCENARIO UI CONTEXT PAGE UI API CONTEXT CLIENT Reusability 39
  • 40. 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" 40
  • 41. 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 41
  • 42. 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); } 42
  • 43. 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()) ; } } 43
  • 44. 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()) ; } } 44 $this ->sharedStorage ->set('last_exception', $exception->getPrevious()) ; And I should be notified that the product has been successfully added
  • 45. @application final readonly class AddToCart { public function __construct( public OrderInterface $cart, public ProductInterface $product, public int $quantity, ) { } } 45
  • 46. @application 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(); } } 46
  • 47. 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); } @application 47
  • 48. 48 final class LatestCartQuery { public function __construct(private OrderRepositoryInterface $orderRepository) { } public function __invoke(): OrderInterface { return $this->orderRepository->findLatestCart(); } } @application
  • 49. 49
  • 50. 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 # ... 50
  • 51. 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 - SyliusBehatContextApplicationCartContext filters: tags: "@shopping_cart && @application" 51
  • 52. 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 - SyliusBehatContextApplicationCartContext filters: tags: "@shopping_cart && @application" 52 - 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 - SyliusBehatContextApplicationCartContext
  • 53. 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 - SyliusBehatContextApplicationCartContext filters: tags: "@shopping_cart && @application" tags: "@shopping_cart && @application" 53
  • 54. default: suites: api_v1_shopping_cart: contexts: - SyliusBehatContextApiV1CartContext filters: tags: "@shopping_cart && @api && @v1” api_v2_shopping_cart: contexts: - SyliusBehatContextApiV2CartContext filters: tags: "@shopping_cart && @api && @v2" 54
  • 57. 57
  • 58. 58
  • 59. 59
  • 61. THANK YOU QR CODE @CommerceWeavers @Sylius QR CODE @mpzalewski Zales0123