Upload
lukasz-chrusciel
View
312
Download
0
Embed Size (px)
Citation preview
AGENDA• Sylius in a nutshell
• Tests and their types
• PHPSpec usage for unit testing
• API testing with ApiTestCase
• New approach to Behat
• Summary
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
REVOLUTIONS?
Fixtures systemShop
Admin panel
Repositories
Factories
Translations
New approach of Behat
User handling
Products
Checkout flow
State machines
TYPES OF TESTS• Unit
• Integration
• Functional
• Stress
• Performance
• Usability
• Acceptance
• Regression
• API
• GUI
EASY TO 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); } }
Acme\AppBundle\Calculator\TypicalCalculator
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; } }
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; } }
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); }
SOMETHING MORE SOPHISTICATED
function it_returns_zero_if_given_reviewable_object_has_no_reviews( ReviewableInterface $reviewable
){ $reviewable->getReviews()->willReturn([]));
$this->calculate($reviewable)->shouldReturn(0); }
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); }
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; } }
COLLABORATORS?namespace Sylius\Bundle\OrderBundle\NumberAssigner;
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)); } }
COLLABORATORS?namespace spec\Sylius\Bundle\OrderBundle\NumberAssigner;
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); } }
WHAT DO WE HAVE?
URL: Raw data to send:
Expected response:
POST /api/countries/ {"code": "BE"} { "id": 1, "code": "BE" }
namespace Sylius\Test\Controller;
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" }
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>
// 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>@[email protected]()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-lotr</loc> <lastmod>@[email protected]()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> <url> <loc>http://sylius.org/products/mug-breaking-bad</loc> <lastmod>@[email protected]()</lastmod> <changefreq>always</changefreq> <priority>0.5</priority> </url> </urlset>
namespace Sylius\Test\Controller;
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'); } }
HELPS IN DEBUGGING
1) Sylius\Tests\Controller\OauthTokenApiTest::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, }
WHAT WAS THAT?
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)]*) "([^""]*)"$/ */ public function iAmOnTheResourcePageByName($type, $name) { if ('country' === $type) { $this->iAmOnTheCountryPageByName($name);
return; } $this->iAmOnTheResourcePage($type, 'name', $name); }
@promotionsFeature: 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 > Debian T-Shirts | And the following products exist: | name | price | taxons | | Woody | 125 | Debian T-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 "[email protected]"
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_discountFeature: Receiving fixed discount on cart In order to pay proper amount while buying promoted goods As a Visitor 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 "PHP T-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 "PHP T-Shirt" to the cart Then my cart total should be "€90.00" And my discount should be "-€10.00"
@receiving_discountFeature: 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 "PHP T-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 "PHP T-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"
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 @uiScenario: 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
TAGSnamespace Sylius\Behat\Context\Domain;
final class OrderContext implements Context { /** * @When I delete the order :order */ public function iDeleteTheOrder(OrderInterface $order) { $this->orderRepository->remove($order); } }
namespace Sylius\Behat\Context\Ui;
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
RECOMENDATIONS
Tags steps should be written in such a waythat can be interpreted in many contexts
Do not create a new object with Transformers
Shared storage container to keep data between contexts
Use ubiquitous language
PAGE AS A SERVICE
namespace Sylius\Behat\Page\Admin\Account;
class LoginPage implements LoginPageInterface { public function specifyPassword($password) { $this->getDocument()->fillField('Password', $password); }
public function specifyUsername($username) { $this->getDocument()->fillField('Username', $username); } }
namespace Sylius\Behat\Context\Ui\Admin;
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); } }
CONTEXT AS A SERVICEdefault: 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 Sylius\Behat\Context\Ui\Admin;
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(){…} }
QUESTIONS?
Łukasz Chruściel @lukaszchruscielhttps://github.com/lchrusciel
Worth to see:http://www.phpspec.net/en/stable/http://docs.behat.org/en/v3.0/https://github.com/Lakion/ApiTestCasehttps://github.com/Sylius/Syliushttp://martinfowler.com/bliki/PageObject.htmlhttp://mink.behat.org/en/latest/https://github.com/FriendsOfBehat