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

Recently uploaded (20)

Understanding Globus Data Transfers with NetSage
Understanding Globus Data Transfers with NetSageUnderstanding Globus Data Transfers with NetSage
Understanding Globus Data Transfers with NetSage
 
Paketo Buildpacks : la meilleure façon de construire des images OCI? DevopsDa...
Paketo Buildpacks : la meilleure façon de construire des images OCI? DevopsDa...Paketo Buildpacks : la meilleure façon de construire des images OCI? DevopsDa...
Paketo Buildpacks : la meilleure façon de construire des images OCI? DevopsDa...
 
A Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdfA Comprehensive Look at Generative AI in Retail App Testing.pdf
A Comprehensive Look at Generative AI in Retail App Testing.pdf
 
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
 
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...
 
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
Gamify Your Mind; The Secret Sauce to Delivering Success, Continuously Improv...
 
AI/ML Infra Meetup | ML explainability in Michelangelo
AI/ML Infra Meetup | ML explainability in MichelangeloAI/ML Infra Meetup | ML explainability in Michelangelo
AI/ML Infra Meetup | ML explainability in Michelangelo
 
Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024Globus Connect Server Deep Dive - GlobusWorld 2024
Globus Connect Server Deep Dive - GlobusWorld 2024
 
Using IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New ZealandUsing IESVE for Room Loads Analysis - Australia & New Zealand
Using IESVE for Room Loads Analysis - Australia & New Zealand
 
GraphAware - Transforming policing with graph-based intelligence analysis
GraphAware - Transforming policing with graph-based intelligence analysisGraphAware - Transforming policing with graph-based intelligence analysis
GraphAware - Transforming policing with graph-based intelligence analysis
 
How Does XfilesPro Ensure Security While Sharing Documents in Salesforce?
How Does XfilesPro Ensure Security While Sharing Documents in Salesforce?How Does XfilesPro Ensure Security While Sharing Documents in Salesforce?
How Does XfilesPro Ensure Security While Sharing Documents in Salesforce?
 
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
 
Accelerate Enterprise Software Engineering with Platformless
Accelerate Enterprise Software Engineering with PlatformlessAccelerate Enterprise Software Engineering with Platformless
Accelerate Enterprise Software Engineering with Platformless
 
First Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User EndpointsFirst Steps with Globus Compute Multi-User Endpoints
First Steps with Globus Compute Multi-User Endpoints
 
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
Exploring Innovations in Data Repository Solutions - Insights from the U.S. G...
 
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
 
Vitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume MontevideoVitthal Shirke Microservices Resume Montevideo
Vitthal Shirke Microservices Resume Montevideo
 
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...
 
2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx2024 RoOUG Security model for the cloud.pptx
2024 RoOUG Security model for the cloud.pptx
 
De mooiste recreatieve routes ontdekken met RouteYou en FME
De mooiste recreatieve routes ontdekken met RouteYou en FMEDe mooiste recreatieve routes ontdekken met RouteYou en FME
De mooiste recreatieve routes ontdekken met RouteYou en FME
 

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