Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
DIVERSIFIED APPLICATION
TESTING
BASED ON A SYLIUS PROJECT
Łukasz Chruściel
AGENDA
• Sylius in a nutshell
• Tests and their types
• PHPSpec usage for unit testing
• API testing with ApiTestCase
• Ne...
SYLIUS
IN A NUTSHELL
WHAT IS SYLIUS?
330+ contributors 10k+ contributions
5 years of development
Still in dev
A few revolutions behind us
60+ p...
REVOLUTIONS?
Fixtures system
Shop
Admin panel
Repositories
Factories
Translations
New approach of Behat
User handling
Prod...
HOW DID WE HANDLE IT?
TESTS
TYPES OFTESTS
• Unit
• Integration
• Functional
• Stress
• Performance
• Usability
• Acceptance
• Regression
• API
• GUI
TYPES OFTESTS
• Unit
• Integration
• Functional
• Acceptance
• API
• GUI
PHPSPECTOTHE RESCUE!
UnitTests?
EASYTO UNDERSTAND
final class TypicalCalculatorSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shoul...
SOMETHING MORE
SOPHISTICATED
class AverageRatingCalculator implements ReviewableRatingCalculatorInterface
{
public functio...
SOMETHING MORE
SOPHISTICATED
function it_calculates_average_price(
ReviewableInterface $reviewable,
ReviewInterface $revie...
SOMETHING MORE
SOPHISTICATED
function it_returns_zero_if_given_reviewable_object_has_no_reviews(
ReviewableInterface $revi...
SOMETHING MORE
SOPHISTICATED
function it_returns_zero_if_given_reviewable_object_has_reviews_but_none_of_them_is_accepted(...
SOMETHING MORE
SOPHISTICATED
class AverageRatingCalculator implements ReviewableRatingCalculatorInterface
{
public functio...
COLLABORATORS?
namespace SyliusBundleOrderBundleNumberAssigner;
final class OrderNumberAssigner implements OrderNumberAssi...
COLLABORATORS?
namespace specSyliusBundleOrderBundleNumberAssigner;
final class OrderNumberAssignerSpec extends ObjectBeha...
WHAT DO WETEST WITH
PHPSPEC?
*Although we don’t spec repositories, nor forms.
EVERYTHING*
„BUT IT IS HARD/IMPOSSIBLE TO SPEC MY CODE!”
THEN SOMETHING SMELLS
REALLY, REALLY BAD
APITESTING WITH
APITESTCASE
JSON?
WHAT DO WE HAVE?
URL:
Raw data to send:
Expected response:
POST /api/countries/
{"code": "BE"}
{
"id": 1,
"code": "BE"
}
namespace SyliusTestController;
class CountryApiTest extends JsonApiTestCase
{
public function testCreateCountryResponse()...
XML?
WHAT DO WE HAVE?
URL:
Expected response:
GET /sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www...
// test/Responses/Expected/sitemap/show_sitemap.xml
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitem...
HELPS IN DEBUGGING
1) SyliusTestsControllerOauthTokenApiTest::it_provides_an_access_token
"3600" does not match "7200".
@@...
NEW APPROACHTO BEHAT
GOOD, OLD BEHAT
WHAT WASTHAT?
Uber Behat contexts
800+ lines of code
Many ancestors
Each Behat context got an `F` degree on scrutinizer
WAS IT WRONG?
/**
* @Given /^I am on the page of ([^""(w)]*) "([^""]*)"$/
* @Given /^I go to the page of ([^""(w)]*) "([^"...
WAS IT WRONG?
IT WAS TERRIBLE!
SOLUTION?
STYLE
@promotions
Feature: Checkout fixed discount promotions
In order to handle product promotions
As a store owner
I want to ap...
@receiving_discount
Feature: Receiving percentage discount on shipping
In order to pay decreased amount for shipping
As a ...
100% BEHAT
TRANSFORMERS
<?php
final class PromotionContext implements Context
{
/**
* @Then promotion :promotion should still exist i...
TAGS
namespace SyliusBehatContextDomain;
final class OrderContext implements Context
{
/**
* @When I delete the order :ord...
RECOMENDATIONS
Tags steps should be written in such a way
that can be interpreted in many contexts
Do not create a new obj...
PAGE OBJECT PATTERN
PAGE AS A SERVICE
namespace SyliusBehatPageAdminAccount;
class LoginPage implements LoginPageInterface
{
public function s...
CONTEXT AS A SERVICE
default:
suites:
ui_administrator_login:
contexts_as_services:
- sylius.behat.context.hook.doctrine_o...
SERVICES FTW!
SUMMARY
QUESTIONS?
Łukasz Chruściel
@lukaszchrusciel
https://github.com/lchrusciel
Worth to see:
http://www.phpspec.net/en/stable/...
Diversified application testing based on a Sylius project
Upcoming SlideShare
Loading in …5
×

Diversified application testing based on a Sylius project

526 views

Published on

Compound information how a Sylius OSS project is tested.

Published in: Software
  • Be the first to comment

  • Be the first to like this

Diversified application testing based on a Sylius project

  1. 1. DIVERSIFIED APPLICATION TESTING BASED ON A SYLIUS PROJECT Łukasz Chruściel
  2. 2. AGENDA • Sylius in a nutshell • Tests and their types • PHPSpec usage for unit testing • API testing with ApiTestCase • New approach to Behat • Summary
  3. 3. SYLIUS IN A NUTSHELL
  4. 4. WHAT IS SYLIUS? 330+ contributors 10k+ contributions 5 years of development Still in dev A few revolutions behind us 60+ packages Full stack BDD OSS Project E-commerce platform
  5. 5. REVOLUTIONS? Fixtures system Shop Admin panel Repositories Factories Translations New approach of Behat User handling Products Checkout flow State machines
  6. 6. HOW DID WE HANDLE IT?
  7. 7. TESTS
  8. 8. TYPES OFTESTS • Unit • Integration • Functional • Stress • Performance • Usability • Acceptance • Regression • API • GUI
  9. 9. TYPES OFTESTS • Unit • Integration • Functional • Acceptance • API • GUI
  10. 10. PHPSPECTOTHE RESCUE! UnitTests?
  11. 11. EASYTO UNDERSTAND final class TypicalCalculatorSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType(Calculator::class); } function it_adds_two_numbers() { $this->add(2, 4)->shouldReturn(6); } } AcmeAppBundleCalculatorTypicalCalculator 25 ✔ is initializable 35 ✔ adds two numbers 1 specs 3 examples (3 passed) 11ms final class TypicalCalculator { public function add($a, $b) { return $a + $b; } }
  12. 12. SOMETHING MORE SOPHISTICATED class AverageRatingCalculator implements ReviewableRatingCalculatorInterface { public function calculate(ReviewableInterface $reviewable) { $sum = 0; $reviewsNumber = 0; $reviews = $reviewable->getReviews(); foreach ($reviews as $review) { if (ReviewInterface::STATUS_ACCEPTED === $review->getStatus()) { ++$reviewsNumber; $sum += $review->getRating(); } } return 0 !== $reviewsNumber ? $sum / $reviewsNumber : 0; } }
  13. 13. SOMETHING MORE SOPHISTICATED function it_calculates_average_price( ReviewableInterface $reviewable, ReviewInterface $review1, ReviewInterface $review2 ) { $reviewable->getReviews()->willReturn([$review1, $review2]); $review1 ->getStatus() ->willReturn(ReviewInterface::STATUS_ACCEPTED); $review2 ->getStatus() ->willReturn(ReviewInterface::STATUS_ACCEPTED); $review1->getRating()->willReturn(4); $review2->getRating()->willReturn(5); $this->calculate($reviewable)->shouldReturn(4.5); }
  14. 14. SOMETHING MORE SOPHISTICATED function it_returns_zero_if_given_reviewable_object_has_no_reviews( ReviewableInterface $reviewable ){ $reviewable->getReviews()->willReturn([])); $this->calculate($reviewable)->shouldReturn(0); }
  15. 15. SOMETHING MORE SOPHISTICATED function it_returns_zero_if_given_reviewable_object_has_reviews_but_none_of_them_is_accepted( ReviewableInterface $reviewable, ReviewInterface $review ) { $reviewable->getReviews()->willReturn([$review]); $review->getStatus()->willReturn(ReviewInterface::STATUS_NEW); $this->calculate($reviewable)->shouldReturn(0); }
  16. 16. SOMETHING MORE SOPHISTICATED class AverageRatingCalculator implements ReviewableRatingCalculatorInterface { public function calculate(ReviewableInterface $reviewable) { $sum = 0; $reviewsNumber = 0; $reviews = $reviewable->getReviews(); foreach ($reviews as $review) { if (ReviewInterface::STATUS_ACCEPTED === $review->getStatus()) { ++$reviewsNumber; $sum += $review->getRating(); } } return 0 !== $reviewsNumber ? $sum / $reviewsNumber : 0; } }
  17. 17. COLLABORATORS? namespace SyliusBundleOrderBundleNumberAssigner; final class OrderNumberAssigner implements OrderNumberAssignerInterface { private $numberGenerator; public function __construct(OrderNumberGeneratorInterface $numberGenerator) { $this->numberGenerator = $numberGenerator; } public function assignNumber(OrderInterface $order) { if (null !== $order->getNumber()) { return; } $order->setNumber($this->numberGenerator->generate($order)); } }
  18. 18. COLLABORATORS? namespace specSyliusBundleOrderBundleNumberAssigner; final class OrderNumberAssignerSpec extends ObjectBehavior { function let(OrderNumberGeneratorInterface $numberGenerator) { $this->beConstructedWith($numberGenerator); } function it_assigns_number_to_order( OrderInterface $order, OrderNumberGeneratorInterface $numberGenerator ) { $order->getNumber()->willReturn(null); $numberGenerator->generate($order)->willReturn('00000007'); $order->setNumber('00000007')->shouldBeCalled(); $this->assignNumber($order); } function it_does_not_assign_number_to_order_with_number( OrderInterface $order, OrderNumberGeneratorInterface $numberGenerator ) { $order->getNumber()->willReturn('00000007'); $numberGenerator->generate($order)->shouldNotBeCalled(); $order->setNumber(Argument::any())->shouldNotBeCalled(); $this->assignNumber($order); } }
  19. 19. WHAT DO WETEST WITH PHPSPEC? *Although we don’t spec repositories, nor forms. EVERYTHING*
  20. 20. „BUT IT IS HARD/IMPOSSIBLE TO SPEC MY CODE!”
  21. 21. THEN SOMETHING SMELLS REALLY, REALLY BAD
  22. 22. APITESTING WITH APITESTCASE
  23. 23. JSON?
  24. 24. WHAT DO WE HAVE? URL: Raw data to send: Expected response: POST /api/countries/ {"code": "BE"} { "id": 1, "code": "BE" }
  25. 25. namespace SyliusTestController; class CountryApiTest extends JsonApiTestCase { public function testCreateCountryResponse() { $data = <<<EOT { "code": "BE" } EOT; $this->client->request('POST', '/api/countries/', [], [], [], $data); $response = $this->client->getResponse(); $this->assertResponse($response, 'country/create_response', Response::HTTP_CREATED); } } // test/Responses/Expected/country/create_response.json { "id": @integer@, "code": "BE" }
  26. 26. XML?
  27. 27. WHAT DO WE HAVE? URL: Expected response: GET /sitemap.xml <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>http://sylius.org/products/mug-star-wars</loc> <lastmod>2015-10-10T00:00:00+02:00</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-lotr</loc> <lastmod>2015-10-04T00:00:00+02:00</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-breaking-bad</loc> <lastmod>2015-10-05T00:00:00+02:00</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> </urlset>
  28. 28. // test/Responses/Expected/sitemap/show_sitemap.xml <?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>http://sylius.org/products/mug-star-wars</loc> <lastmod>@string@.isDateTime()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-lotr</loc> <lastmod>@string@.isDateTime()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-breaking-bad</loc> <lastmod>@string@.isDateTime()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> </urlset> namespace SyliusTestController; class SitemapControllerApiTest extends XmlApiTestCase { public function testShowActionResponse() { $this->loadFixturesFromFile('resources/product.yml'); $this->client->request('GET', '/sitemap.xml'); $response = $this->client->getResponse(); $this->assertResponse($response, 'sitemap/show_sitemap'); } }
  29. 29. HELPS IN DEBUGGING 1) SyliusTestsControllerOauthTokenApiTest::it_provides_an_access_token "3600" does not match "7200". @@ -1,7 +1,7 @@ { - "expires_in": 7200, + "expires_in": 3600, "token_type": "bearer", "scope": null, }
  30. 30. NEW APPROACHTO BEHAT
  31. 31. GOOD, OLD BEHAT
  32. 32. WHAT WASTHAT? Uber Behat contexts 800+ lines of code Many ancestors Each Behat context got an `F` degree on scrutinizer
  33. 33. WAS IT WRONG? /** * @Given /^I am on the page of ([^""(w)]*) "([^""]*)"$/ * @Given /^I go to the page of ([^""(w)]*) "([^""]*)"$/ */ public function iAmOnTheResourcePageByName($type, $name) { if ('country' === $type) { $this->iAmOnTheCountryPageByName($name); return; } $this->iAmOnTheResourcePage($type, 'name', $name); }
  34. 34. WAS IT WRONG? IT WAS TERRIBLE!
  35. 35. SOLUTION?
  36. 36. STYLE
  37. 37. @promotions Feature: Checkout fixed discount promotions In order to handle product promotions As a store owner I want to apply promotion discounts during checkout Background: Given store has default configuration And the following countries exist: | name | | Germany | | Poland | And there are following taxonomies defined: | name | | Category | And taxonomy "Category" has following taxons: | Clothing > DebianT-Shirts | And the following products exist: | name | price | taxons | | Woody | 125 | DebianT-Shirts | And the following promotions exist: | name | description | | 3 items | Discount for orders with at least 3 items | And all products are assigned to the default channel And all promotions are assigned to the default channel And promotion "3 items" has following rules defined: | type | configuration | | Item count | Count: 3,Equal: true | And promotion "3 items" has following actions defined: | type | configuration | | Fixed discount | Amount: 15 | And I am logged in as user "klaus@example.com" Scenario: Fixed discount promotion is applied when the cart has the required amount Given I am on the store homepage When I add product "Woody" to cart, with quantity "3" Then I should be on the cart summary page And "Promotion total: -€40.00" should appear on the page And "Grand total: €295.00" should appear on the page @receiving_discount Feature: Receiving fixed discount on cart In order to pay proper amount while buying promoted goods As aVisitor I want to have promotions applied to my cart Background: Given the store operates on a single channel in "France" And the store has a product "PHPT-Shirt" priced at "€100.00" And the store has a product "PHP Mug" priced at "€6.00" @ui Scenario: Receiving fixed discount for my cart Given there is a promotion "Holiday promotion" And it gives "€10.00" discount to every order When I add product "PHPT-Shirt" to the cart Then my cart total should be "€90.00" And my discount should be "-€10.00"
  38. 38. @receiving_discount Feature: Receiving percentage discount on shipping In order to pay decreased amount for shipping As a Customer I want to have shipping promotion applied to my cart Background: Given the store operates on a single channel in "France" And the store has "DHL" shipping method with "€10.00" fee And the store has a product "PHPT-Shirt" priced at "€100.00" And there is a promotion "Holiday promotion" And I am a logged in customer @ui Scenario: Receiving percentage discount on shipping Given the promotion gives "20%" discount on shipping to every order When I add product "PHPT-Shirt" to the cart And I proceed selecting "DHL" shipping method Then my cart total should be "€108.00" And my cart shipping total should be "€8.00"
  39. 39. 100% BEHAT
  40. 40. TRANSFORMERS <?php final class PromotionContext implements Context { /** * @Then promotion :promotion should still exist in the registry */ public function promotionShouldStillExistInTheRegistry(PromotionInterface $promotion) { Assert::notNull($this->promotionRepository->find($promotion->getId())); } } <?php final class PromotionContext implements Context { /** * @Transform :promotion */ public function getPromotionByName($promotionName) { return $this->promotionRepository->findOneBy(['name' => $promotionName]); } } @domain @ui
 Scenario: Being unable to delete a promotion that was applied to an order
 When I try to delete a "Christmas sale" promotion
 Then I should be notified that it is in use and cannot be deleted
 And promotion "Christmas sale" should still exist in the registry
  41. 41. TAGS namespace SyliusBehatContextDomain; final class OrderContext implements Context { /** * @When I delete the order :order */ public function iDeleteTheOrder(OrderInterface $order) { $this->orderRepository->remove($order); } } namespace SyliusBehatContextUi; final class OrderContext implements Context { /** * @When I delete the order :order */ public function iDeleteTheOrder(OrderInterface $order) { $this->showPage->open(['id' => $order->getId()]); $this->showPage->deleteOrder(); } } @domain @ui Scenario: Deleted order should disappear from the registry When I delete the order "#00000022" Then I should be notified that it has been successfully deleted And this order should not exist in the registry
  42. 42. RECOMENDATIONS Tags steps should be written in such a way that can be interpreted in many contexts Do not create a new object withTransformers Shared storage container to keep data between contexts Use ubiquitous language
  43. 43. PAGE OBJECT PATTERN
  44. 44. PAGE AS A SERVICE namespace SyliusBehatPageAdminAccount; class LoginPage implements LoginPageInterface { public function specifyPassword($password) { $this->getDocument()->fillField('Password', $password); } public function specifyUsername($username) { $this->getDocument()->fillField('Username', $username); } } namespace SyliusBehatContextUiAdmin; final class LoginContext implements Context { public function __construct(LoginPageInterface $loginPage) { $this->loginPage = $loginPage; } 
 /** @When I specify the username as :username */ public function iSpecifyTheUsername($username) { $this->loginPage->specifyUsername($username); } 
 /** @When I specify the password as :password */ public function iSpecifyThePasswordAs($password) { $this->loginPage->specifyPassword($password); } }
  45. 45. CONTEXT AS A SERVICE default: suites: ui_administrator_login: contexts_as_services: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.user - sylius.behat.context.setup.channel - sylius.behat.context.setup.admin_user - sylius.behat.context.setup.user - sylius.behat.context.ui.admin.login filters: tags: "@administrator_login && @ui" namespace SyliusBehatContextUiAdmin; final class LoginContext implements Context { public function __construct(LoginPageInterface $loginPage) { $this->loginPage = $loginPage; } /** * @Given I want to log in */ public function iWantToLogIn(){…} /** * @When I specify the username as :username */ public function iSpecifyTheUsername($username){…} /** * @When I specify the password as :password */ public function iSpecifyThePasswordAs($password){…} /** * @When I log in */ public function iLogIn(){…} }
  46. 46. SERVICES FTW!
  47. 47. SUMMARY
  48. 48. QUESTIONS? Łukasz Chruściel @lukaszchrusciel https://github.com/lchrusciel Worth to see: http://www.phpspec.net/en/stable/ http://docs.behat.org/en/v3.0/ https://github.com/Lakion/ApiTestCase https://github.com/Sylius/Sylius http://martinfowler.com/bliki/PageObject.html http://mink.behat.org/en/latest/ https://github.com/FriendsOfBehat

×