Key Takeaways

  • Sylius, an e-commerce application built on Symfony, demonstrates a robust approach to TDD with 100% code coverage, setting a high standard for PHP applications.
  • The article provides a practical walkthrough on setting up a test environment in Sylius, including creating a test database and configuring Behat, PHPUnit, and phpspec tests.
  • It emphasizes the importance of Behavior Driven Development (BDD) and Test Driven Development (TDD) within Sylius, showcasing how both testing approaches help maintain and improve code quality.
  • Detailed examples illustrate how to modify existing features and add new functionalities to the Sylius platform using TDD principles, ensuring that all changes are driven by predefined tests.
  • The conclusion reinforces the effectiveness of TDD in developing reliable, bug-free applications, encouraging a test-first approach in software development practices.

This article was peer reviewed by Christopher Pitt and Thomas Punt. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!


Sylius logo

Sylius is an e-commerce application / framework based on Symfony. It boasts 100% code coverage, which is impressive for a PHP application of that size. In this article, we are going to walk through the different kinds of tests available and try out some Test and Behavior Driven Development (TDD/BDD). See the Sylius installation guide page for instructions, as this article assumes you have a working installation with example data and you can run Behat, PHPUnit and phpspec tests.

In the web root, there’s a src folder which holds all Sylius-related code. This allows you to make use of the app folder for your application development without unnecessarily treading on Sylius’ toes. As we are interested in test-driven development (first, write tests that fail before writing the code) let’s dive in, the Sylius way.

We start by setting up our test database.

php bin/console doctrine:database:create --env=test
php bin/console doctrine:schema:create --env=test

Types of Sylius Tests

Some of the basics of the tools below have already been covered in this post, but we’ll recap them here on Sylius examples to keep with the theme.

PHPUnit

Sylius comes with a lot of PHPUnit functional tests. The configuration file, phpunit.xml.dist, is in the web root and the unit tests are in the tests folder. From the root of our application, let’s run tests in tests/Controller/CountryApiTest.php:

./vendor/phpunit/phpunit/phpunit -c ./phpunit.xml.dist tests/Controller/CountryApiTest

PHPUnit in action

The command is made up of 3 parts – path to PHPUnit, our configuration file and the unit test class. Open tests/Controller/CountryApiTest.php and take a look at the class. It extends JsonApiTestCase which we can trace back to ApiTestCase and WebTestCase, part of the Symfony framework package.

Phpspec

Behavior Driven Development (BDD) emerged from Test Driven Development (TDD), focusing attention on how a user interacts with an application and how the application behaves. SpecBDD is the part of BDD which considers how the smaller bits of an application work together to make things happen.

Sylius is installed with phpspec which is the tool required for this. In your root directory, there’s also the phpspec.yml.dist configuration file. Specifications are written in PHP classes and may be grouped in suites.

Remember, Sylius is a big application so there are a lot of files. Open src/Sylius/Component/Order/spec/Model/OrderItemSpec.php.

The first thing to note is that no matter how deep the folder structure, you have specifications inside a spec folder and the source code the tests apply to is easy to find. If you look at the level of the spec folder, you’ll see Model and inside it is an OrderItem class. The spec for that class is spec/Model/OrderItemSpec.php. Compare the functions and you can see how they are related.

When you run phpspec, you get more output with the --verbose option and with -fpretty you can get prettier outputs.

./bin/phpspec run -fpretty --verbose src/Sylius/Component/Order/spec/Model/OrderItemSpec.php

PHPSpec in action in Sylius

Behat

StoryBDD is the second side of the BDD coin which presents behavior in a narrative, focusing attention on the bigger picture of what should happen in the application. Behat is the primary tool that makes StoryBDD possible. You run Behat, it parses special files called features, and checks whether the behavior of your website matches the description in those files. Other tools are required to mimic or emulate the functionality of browsers. Mink is the library that Behat needs for that, while something like Laravel will use the excellent Dusk.

The description of the behavior you write is in a format called Gherkin. Each behavior is called a scenario, and a single feature file can have multiple scenarios. The files must have an extension of feature.

Sylius comes with a behat.yml.dist configuration file and a features folder with sub-folders where feature files are organized. The configuration file and features folder must be at the same level. Open features/order/managing_orders/browsing_orders.feature and look at the structure.

Let’s give this feature a test drive:

./bin/behat features/order/managing_orders/browsing_orders.feature

Feature being tested

See BDD in Laravel: Getting Started with Behat and PhpSpec for another great article on this subject.

Let’s Do TDD

We’re going to modify the Orders list page the test-driven way. Log into the admin section, admin/login. The icon in the top left corner looks perfect, but let’s try and change it to something else. Sylius uses Semantic UI for its front-end. Take a look at the icon set. Let’s say we want to replace the “shop” with “shopping basket” icon. Viewing the source of the Orders list page, we want to go from <i class="circular shop icon"></i> to <i class="circular shopping basket icon"></i>.

Step 1: Add a Feature

We aren’t starting from a blank page so we go the Sylius way. Order-related features are in features\order\managing_orders, so we create a file there and call it browsing_orders_with_visual_display.feature and add:

@viewing_page_icon
Feature: Browsing orders page with icon
    In order to identify orders page
    As an Administrator
    I want to be able to see an icon on the orders page

    Background:
        Given the store operates on a single channel in "United States"
        And the store has a product "PHP T-Shirt"
        And the store ships everywhere for free
        And the store allows paying with "Cash on Delivery"
        And there is a customer "john.doe@gmail.com" that placed an order "#00000022"
        And the customer bought a single "PHP T-Shirt"
        And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment
        And I am logged in as an administrator

    @ui
    Scenario: Seeing an icon on the page
        When I browse orders
        Then the "shopping basket" icon should be visible on the page

The Background sets the stage for the tests. For this to work, it needs to include the channel, product, shipping method, customer details, and the user who wants to view the page. Luckily enough Sylius has done the ground work for us.

Step 2: Add a Page Object

Sylius allows us to create new page objects defined in the Behat container in a etc\behat\services\pages.xml file. We’re going to create an interface and implementation in src\Sylius\Behat\Page\Admin\Order\.

<container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    <parameters>
        <parameter key="sylius.behat.page.admin.order.order_page.class">Sylius\Behat\Page\Admin\Order\OrderPage
        </parameter>
    </parameters>

    <services>
        <service id="sylius.behat.page.admin.order.order_page" class="%sylius.behat.page.admin.order.order_page.class%"
                 parent="sylius.behat.page.admin.order.index" public="false">
        </service>

    </services>
</container>

We’re informing the Behat service container about our OrderPage class. Let’s create src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php:

<?php

// src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php

namespace Sylius\Behat\Page\Admin\Order;

use Sylius\Behat\Page\SymfonyPageInterface;
use Sylius\Behat\Page\Admin\Crud\IndexPageInterface;

interface OrderPageInterface extends IndexPageInterface
{

    /**
     * Match icons by class.
     *
     * @param string $class
     *  The class to match icons against.
     *
     * @return mixed
     *  The matched icons or false.
     */
     public function findIcons($class);

}

Then, src/Sylius/Behat/Page/Admin/Order/OrderPage.php:

<?php

// src/Sylius/Behat/Page/Admin/Order/OrderPage.php

namespace Sylius\Behat\Page\Admin\Order;

use Behat\Mink\Exception\ElementNotFoundException;
use Behat\Mink\Session;
use Sylius\Behat\Page\SymfonyPage;
use Sylius\Behat\Service\Accessor\TableAccessorInterface;
use Symfony\Component\Routing\RouterInterface;
use Sylius\Behat\Page\Admin\Crud\IndexPage;

class OrderPage extends IndexPage implements OrderPageInterface
{

    /**
     * @inheritDoc
     */
    public function findIcons($class)
    {
        $foundIcons = $this->getElement('icon')->find('css', '.' . implode('.', explode(' ', $class)));
        if (!$foundIcons) {
                throw new ElementNotFoundException($this->getSession(), 'Icons with class(es) ' . $class);
            }

        return $foundIcons;
    }

    /**
     * @inheritDoc
     */
    protected function getDefinedElements()
    {
        return array_merge(parent::getDefinedElements(), [
            'icon' => '.icon',
        ]);
    }
}

Sylius has defined over 120 elements (classes and IDs) for identifying different things on pages but apparently not icons. (Search src\Sylius\Behat for them). We need to find the icon class so we define our own in the getDefinedElements() method. Our findIcons() method looks for the class we pass in from our feature. When icons have more than one name, such as “shopping basket”, we need to find both names.

Step 3: Add a Context for the Tests to Run

A context is a class that is also provided as a service. Starting from src/Sylius/Behat/Resources/config/services.xml you’ll find <import resource="services/contexts.xml" /> near the top. Following the subsequent imports, you’ll end up in src/Sylius/Behat/Resources/config/services/contexts/ where some context categories have been configured (cli.xml, domain.xml, hook.xml, setup.xml, transform.xml and ui.xml). We’re after the ui.xml one. Open it. Find the sylius.behat.context.ui.admin.managing_orders service and we add ours just below it:

<service id="sylius.behat.context.ui.admin.viewing_page_icon" class="Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext">
    <argument type="service" id="sylius.behat.page.admin.order.order_page" />
    <tag name="fob.context_service" />
</service>

Note the relationship between the service id and class. Dots are replaced with backslashes and camel-casing of class names.

Create src\Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext.php and add this:

<?php

// src/Sylius/Behat/Context/Ui/Admin/ViewingPageIconContext.php

namespace Sylius\Behat\Context\Ui\Admin;

use Behat\Behat\Context\Context;
use Sylius\Behat\Page\Admin\Order\OrderPageInterface;
use Webmozart\Assert\Assert;
use Behat\Behat\Tester\Exception\PendingException;

class ViewingPageIconContext implements Context
{
    /**
     * @var OrderPageInterface
     */
    private $orderPage;

    /**
     * @param OrderPageInterface $orderPage
     */
    public function __construct(OrderPageInterface $orderPage)
    {
        $this->orderPage = $orderPage;
    }

    /**
     * @Then /^the "([^"]*)" icon should be visible on the page$/
     */
    public function theIconShouldBeVisibleOnThePage($class)
    {
        $icons = $this->orderPage->findIcons($class);
        Assert::eq(count($icons), 1, sprintf('Icon with class, %s, not found', $class));
    }
}

We inject our OrderPageInterface into this class so that when Behat finds our scenario, we’re able to call the right method from the injected class. We expect only one icon and we assert it.

Let’s run our test. It fails because there’s no shopping basket icon yet. Now we can go ahead and make the change. Open src/Sylius/Bundle/AdminBundle/Resources/config/routing/order.yml and change icon: cart to icon: shopping basket. Run the test and it should be green.

A passing icon test

Next, we make a couple of other changes. Our store is a single-channel business, so we don’t need the Channel column. Secondly, we want the table row to have a different background depending on the state. For this, we’ll write another test before making the change. Cancel one or two orders in the admin section so that we have different states.

  • Update our feature file features\order\managing_orders\browsing_orders_with_visual_display.feature with this:

    @ui
    Scenario: Checking row of a cancelled order
        When I view the summary of the order "#00000022"
        And I cancel this order
        When I browse orders
        Then this order should have order payment state "Cancelled"
        And the row should be marked with the order payment state "Cancelled"
    
  • We’ll also update OrderPageInterface and OrderPage:

    // src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php
    
    /**
         * Match table rows by class.
         *
         * @param string $class
         *  The class to match table rows against.
         *
         * @return mixed
         *  The matched rows or false.
         */
    public function findTableRows($class);
    
    // src/Sylius/Behat/Page/Admin/Order/OrderPage.php
    
    /**
         * @inheritDoc
         */
    public function findTableRows($class)
    {
            $foundRows = $this->getElement('table')->find('css', '.' . $class);
            if (!$foundRows) {
                throw new ElementNotFoundException($this->getSession(), 'Rows with class ' . $class);
            }
            return $foundRows;
    }
    

    The table element is already defined in Sylius so we just use it. We find all rows with the class passed in. When you run the test, it’s red. Now we can make the necessary changes.

  • Override “State” and “Channel” columns in the sylius_admin_order grid, defined in src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml.

    • Create app\config\grids.yml and add this:
        sylius_grid:
            grids:
                sylius_admin_order:
                    fields:
                        channel:
                            enabled: false
                        state:
                            enabled: false
    
    • Tell Sylius about this file by importing it in app\config\config.yml:
        - { resource: "grids.yml" }
    
  • Copy src\Sylius\Bundle\UiBundle\Resources\views\Macro\table.html.twig to app\Resources\SyliusUiBundle\views\Macro\table.html.twig and in the {% macro row(grid, definition, row) %} statement, replace <tr class="item"> with <tr class="item {{ row.state | lower }}">. All we have to do is add CSS for the output classes here.

  • Copy src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\PaymentState\awaiting_payment.html.twig to app\Resources\SyliusAdminBundle\views\Order\Label\PaymentState\awaiting_payment.html.twig. We want the icon and the label to be uppercase. Change the file to this:

    <span class="ui olive label">
        {{ value|trans|upper }}
    </span>
    
  • Copy src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\PaymentState\cancelled.html.twig to app\Resources\SyliusAdminBundle\views\Order\Label\PaymentState\cancelled.html.twig. Change the file to this:

    <span class="ui yellow label">
        {{ value|trans|upper }}
    </span>
    

  • Copy src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\ShippingState\cancelled.html.twig to app\Resources\SyliusAdminBundle\views\Order\Label\ShippingState\cancelled.html.twig. We do the same as above:

    <span class="ui yellow label">
        {{ value|trans|upper }}
    </span>
    
  • Copy src\Sylius\Bundle\AdminBundle\Resources\views\Order\Label\ShippingState\ready.html.twig to app\Resources\SyliusAdminBundle\views\Order\Label\ShippingState\ready.html.twig. We do the same as above:

    <span class="ui blue label">
        {{ value|trans|upper }}
    </span>
    
  • Copy src\Sylius\Bundle\UiBundle\Resources\views\_stylesheets.html.twig to app\Resources\SyliusUiBundle\views\_stylesheets.html.twig. Here we add our custom CSS file just below the default: <link rel="stylesheet" href="{{ asset('assets/admin/css/style.css') }}">. There are other ways of doing this but this is the quickest way to keep our changes isolated.

  • Add assets/admin/css/style.css with our magnificent CSS.

    .order-row.new {
        background-color: #c5effd;
    }
    
    .order-row.cancelled {
        background-color: #fbe7af;
    }
    

Run the test again. It’s all green.

Refresh the orders page but you may also need to clear the cache first. Cancelled orders rows now have a different background color.

Finally, let’s add a test to assert that the Channel column is no more.

  1. Update our feature file features\order\managing_orders\browsing_orders_with_visual_display.feature with this:

    Scenario: Not seeing a column on the page
        When I browse orders
        Then the "channel" column should not be visible on the page
    
  2. Update OrderPageInterface.php with:

    // src/Sylius/Behat/Page/Admin/Order/OrderPageInterface.php
    
    /**
     * Get table headers.
     *
     * @return mixed
     *  The array of headers keys or empty array.
     */
    public function findTableHeaders();  
    
  3. Implement the new method in OrderPage.php:

    // src/Sylius/Behat/Page/Admin/Order/OrderPage.php
    
    /**
     * {@inheritdoc}
     */
    public function findTableHeaders() {
        $headers = $this->getTableAccessor()
            ->getSortableHeaders($this->getElement('table'));
        return is_array($headers) ? array_keys($headers) : array();
    }  
    

    Sylius comes with a TableAccessor class with a lot of useful methods for working with tables in tests. The default table headers are sortable and we simply call the method that fetches all sortable th elements on the page.

  4. Update our feature context ViewingPageIconContext.php:

      // src\Sylius\Behat\Context\Ui\Admin\ViewingPageIconContext.php
    
      /**
       * @Then /^the "([^"]*)" column should not be visible on the page$/
       */
      public function theColumnShouldNotBeVisibleOnThePage($column) {
        $foundHeaders = $this->orderPage->findTableHeaders();
        Assert::false(in_array(strtolower($column), $foundHeaders), sprintf('%s column not found', $column));
      }
    

We get all table headers on the order page. The array of header elements is keyed by name e.g. date, customer, channel, number, paymentState, shippingState etc. representing the columns. We check the keys to confirm that our column is no longer on the page.

A passing Behat test

Conclusion

These are baby steps designed to illustrate a TDD process – think of what you want to achieve, write tests to check when it’s done, tests fail, make the changes required, and tests pass.

We’ve explored StoryBDD by writing a couple of Behat tests to demonstrate this process. Although these are simple examples, the same principles apply when doing more complex things or working with SpecBDD or PHPUnit tests.

Which approach do you take when testing? Only TDD? Only BDD? Both? Maybe BDDed TDD? Let us know!

Frequently Asked Questions (FAQs) about Sylius and Test-Driven Development (TDD)

What is Sylius and why should I use it for my eCommerce platform?

Sylius is an open-source eCommerce platform built on the Symfony framework. It offers a high level of flexibility and customization, allowing you to tailor your online store to your specific needs. Sylius is designed to be easy to use, with a clean, intuitive interface and a wide range of features. It also supports a variety of payment gateways, making it easy to accept payments from customers all over the world.

How does Test-Driven Development (TDD) work in Sylius?

TDD is a software development approach where tests are written before the actual code. In Sylius, you can use tools like PHPSpec and Behat for TDD. These tools allow you to write specifications for your code, which are then used to guide the development process. This approach helps to ensure that your code is robust, reliable, and meets the specified requirements.

What are the benefits of using TDD in Sylius?

TDD can significantly improve the quality of your code. By writing tests first, you can ensure that your code meets the specified requirements and behaves as expected. This can help to reduce the number of bugs and errors in your code, making your eCommerce platform more reliable and efficient. TDD also encourages good coding practices, such as modular design and code reuse, which can make your code easier to maintain and extend.

How can I start developing plugins for Sylius?

Sylius provides a comprehensive guide for plugin development. This guide covers everything from setting up your development environment to creating your first plugin. It also provides detailed instructions on how to use the Sylius plugin system, which allows you to extend the functionality of your eCommerce platform without modifying the core code.

Where can I find documentation for Sylius?

Sylius provides extensive documentation on its official website. This documentation covers everything from installation and configuration to advanced topics like plugin development and TDD. It’s a great resource for anyone looking to learn more about Sylius and how to use it effectively.

What kind of support is available for Sylius?

Sylius offers a variety of support options, including a community forum, a Slack channel, and professional support services. The community forum and Slack channel are great places to ask questions and get help from other Sylius users, while the professional support services offer dedicated assistance for more complex issues.

How can I contribute to the Sylius project?

Sylius is an open-source project, and contributions are always welcome. You can contribute in a variety of ways, including writing code, reporting bugs, improving documentation, and helping to translate Sylius into other languages. All contributions are reviewed and approved by the Sylius team before being included in the project.

How does Sylius compare to other eCommerce platforms?

Sylius stands out for its flexibility, customization options, and strong support for TDD. Unlike many other eCommerce platforms, Sylius allows you to tailor your online store to your specific needs, rather than forcing you to adapt to a predefined structure. It also supports a wide range of payment gateways, making it easy to accept payments from customers all over the world.

Can I use Sylius for a large-scale eCommerce platform?

Yes, Sylius is designed to handle eCommerce platforms of all sizes, from small online stores to large-scale eCommerce platforms. It offers a scalable architecture, robust performance, and a wide range of features, making it a great choice for any eCommerce project.

Is Sylius suitable for beginners?

While Sylius does have a learning curve, it’s designed to be as user-friendly as possible. The extensive documentation, community support, and clear, intuitive interface make it accessible to beginners. However, some knowledge of PHP and the Symfony framework will be beneficial.

Deji AkalaDeji Akala
View Author

Deji, a Zend Certified PHP Engineer, works as a Software Engineer with the British Council in London. He's passionate about Open Source, contributes to Drupal and speaks at Drupal Camps. He has worked as a dentist, teacher of English as a Foreign Language and radio journalist. Oh yes, he's four times a dad and supports Arsenal FC.

BDDbehatBrunoSE-commerceOOPHPPHPphpspecphpunitsyliussymfonysymfony2symfony3tddunit testing
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week