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 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
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
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
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.
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
andOrderPage
:// 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 insrc/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" }
- Create
Copy
src\Sylius\Bundle\UiBundle\Resources\views\Macro\table.html.twig
toapp\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
toapp\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
toapp\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
toapp\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
toapp\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
toapp\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.
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
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();
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 sortableth
elements on the page.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.
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, 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.