Working with BDD and resources in Sylius - Part 1, Behat

Mikołaj Król, 27 cze 2017

Installation

To setup the environment we will be working with, install a standard Sylius beta-2 version from this guide. The only thing you should do after installation is running install commands with test flag:

$ bin/console sylius:install --env test

Setting up environment

First things first. Let's create the src/Behat directory and register new namespace we will put all Behat-related stuff in.

Add new namespace to your composer.json autoload:

...

"autoload": {
    "psr-4": {
        ...
        "App\\Behat\\": "src/Behat/"
    },
...
}

...

...and dump the autoloader with

$ composer dump-autoload

Now create a new behat.yml file at the root of your project. Copy the content of your existing behat.yml.dist file and paste it to the behat.yml

Create three new files in src/Behat/Resources directory:

  • - suites.yml
  • - contexts.yml
  • - pages.yml
  The contexts and pages files can be used with XML. Feel free to choose the format you are more familiar with.

Import those files in your behat.yml file

imports:
    - "vendor/sylius/sylius/behat.yml.dist"
    - "src/Behat/Resources/suites.yml" #here

default:
    extensions:
        FriendsOfBehat\ContextServiceExtension:
            imports:
                - "vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml"
                - "src/Behat/Resources/pages.yml" #here
                - "src/Behat/Resources/contexts.yml" #and here :)

        FriendsOfBehat\SuiteSettingsExtension:
            paths:
                # - "vendor/sylius/sylius/features" We don't need to run all Sylius tests in this tutorial, it would take a little while there are many of them :)
                - "features"

        Behat\MinkExtension:
            files_path: "%paths.base%/vendor/sylius/sylius/src/Sylius/Behat/Resources/fixtures/"

cached:
    extensions:
        FriendsOfBehat\ContextServiceExtension:
            imports:
                - "vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml"

        Behat\MinkExtension:
            javascript_session: chromium
            files_path: "%paths.base%/vendor/sylius/sylius/src/Sylius/Behat/Resources/fixtures/"

Create an adding_new_book.feature file in your features directory with the following content:

@managing_books
Feature: Adding new book to the store
  In order to add new book to the store
  As an admin
  I want to be able to store the data about this book

  Background:
    Given I am logged in as an administrator

  @ui
  Scenario: Creating new book
    When I open the create new book page
    And I fill the book title with "Harry Potter"
    And I add it
    Then I should be notified that new book was created

Keywords started with @ are tags that help Behat to use proper suite for particular feature.

The basic description after the Feature keyword is something that only helps you order your idea about the feature and makes it more readable for those, who will read it in future.

Background is a keyword that helps you set up the app state which enables the described feature to execute. Think about it as a big if(...) for the whole feature.

Every scenario can also have its single background. Here you can imagine it as if(...) { if(...) {} }. Be careful with nesting too many condition statements! :)

Add the following content to your suites.yml file:

default:
    suites:
        ui_managing_books:
            contexts_services:
                - sylius.behat.context.setup.admin_security

                - app.behat.context.book
            filters:
                tags: "@managing_books && @ui"

A suite is something that wears your feature in single contexts - the classes that transform written statements into some actions in your app. Note also that I was not lying when I was talking about tags :)

Let's create your BookContext

In src/Behat/Context directory create a BookContext.php file with the following implementation:

namespace App\Behat\Context;

use Behat\Behat\Context\Context;

final class BookContext implements Context
{
}

Register this context as a service in your app/Behat/contexts.yml file:

services:
    app.behat.context.book:
        class: App\Behat\Context\BookContext
        tags:
            - { name: fob.context_service }

Run the $ bin/behat features/adding_new_book.feature --append-snippets --snippets-type=turnip in order to generate steps definition in your BookContext class. After running this command you should see something like this:

$ bin/behat

@managing_books
Feature: Adding new book to the store
  In order to add new book to the store
  As an admin
  I want to be able to store the data about this book

  Background:
    Given I am logged in as an administrator

  @ui
  Scenario: Creating new book
    When I open the create new book page
    And I fill the book title with "Harry Potter"
    And I add it
    Then I should be notified that new book was created

1 scenario (1 undefined)
5 steps (1 passed, 4 undefined)
0m1.65s (39.54Mb)

 >> ui_managing_books suite has undefined steps. Please choose the context to generate snippets:

  [0] None
  [1] Sylius\Behat\Context\Setup\AdminSecurityContext
  [2] App\Behat\Context\BookContext
 >

Select the [2] option in order to generate the snippets in the context service we want them to be in.

Now some new methods should appear in your BookContext file. Magic. After getting rid of Pending exception and renaming the :arg1 snippet to :title we should be done with something like this:

namespace App\Behat\Context;

use Behat\Behat\Context\Context;

final class BookContext implements Context
{
    /**
     * @When I open the create new book page
     */
    public function iOpenTheCreateNewBookPage()
    {

    }

    /**
     * @When I fill the book title with :title
     */
    public function iFillTheBookTitleWith($title)
    {

    }

    /**
     * @When I add it
     */
    public function iAddIt()
    {

    }

    /**
     * @Then I should be notified that new book was created
     */
    public function iShouldBeNotifiedThatNewBookWasCreated()
    {

    }
}

Here all the magic start. Now we can implement the behavior of steps we defined in our feature file. To do so, let's create a Page class.

Create the CreatePageInterface in the App\Behat\Page\Book namespace:

namespace App\Behat\Page\Book;

use Sylius\Behat\Page\Admin\Crud\CreatePageInterface as BaseCreatePageInterface;

interface CreatePageInterface extends BaseCreatePageInterface
{
    /**
     * @param string $title
     */
    public function fillWithTitle($title);

    /**
     * @void
     */
    public function add();
}

Now, let's use this interface. Create the CreatePage class in the same namespace:

namespace App\Behat\Page\Book;

use Sylius\Behat\Page\Admin\Crud\CreatePage as BaseCreatePage;

final class CreatePage extends BaseCreatePage implements CreatePageInterface
{
    /**
     * {@inheritdoc}
     */
    public function fillWithTitle($title)
    {
    }

    /**
     * {@inheritdoc}
     */
    public function add()
    {
    }
}

Sylius contains a bunch of traits and Ready to extend page classes. The one we would like to use is the CreatePage from Admin Crud. Behat uses Mink which enables you to write some page interaction which helps you to manipulate the page like for example jQuery does. Here you can open a page, fill field or click a button. Something you probably know from the default functional tests environment in Symfony.

Let's implement the page interaction methods:

// src/Behat/Context/BookContext.php

use Sylius\Behat\Page\Admin\Crud\CreatePage as BaseCreatePage;

final class CreatePage extends BaseCreatePage implements CreatePageInterface
{
    /**
     * {@inheritdoc}
     */
    public function fillWithTitle($title)
    {
        $this->getDocument()->fillField('Title', $title);
    }

    /**
    * {@inheritdoc}
    */
    public function add()
    {
        $this->getDocument()->pressButton('Create');
    }
}

Ok. Here we are. The only thing we now have to do is to inject this page into our BookContext and call the implemented methods for the right steps. To do so, let's register the page as a service first. As this page extends the admin page, let's give this service a parent. The parent constructor requires four parameters. In our example three of them are generic. The only one we need to provide is the route name. Let's name it app_admin_book_create. We will also prevent this page from being visible in the container by setting the public flag to false.

services:
    app.behat.page.create_book:
        class: App\Behat\Page\Book\CreatePage
        parent: sylius.behat.page.admin.crud.create
        public: false
        arguments:
            - "app_admin_book_create"

Let's inject it into BookContext constructor in the class file:

// src/Behat/Context/BookContext.php

final class BookContext implements Context
{
    /**
    * @var CreatePageInterface
    */
    private $createPage;

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

...
}

...and context service

# src/Behat/Resources/contexts.yml
services:
    app.behat.context.book:
        class: App\Behat\Context\BookContext
        arguments:
            - "@app.behat.page.create_book"
        tags:
            - { name: fob.context_service }

Let's now call proper methods in proper steps. we will use the NotificationChecker service in the iShouldBeNotifiedThatNewBookWasCreated() method. Before I can use it, let's jump once again to the context.yml and inject this service.

# src/Behat/Resources/contexts.yml
...
    arguments:
        - "@app.behat.page.create_book"
        - "@sylius.behat.notification_checker"

Now finally let's end the context definition. After all it should look like this:

...

// src/Behat/Context/BookContext.php
final class BookContext implements Context
{
    /**
     * @var CreatePageInterface
     */
    private $createPage;

    /**
     * @var NotificationCheckerInterface
     */
    private $notificationChecker;

    /**
     * @param CreatePageInterface $createPage
     * @param NotificationCheckerInterface $notificationChecker
     */
    public function __construct(
        CreatePageInterface $createPage,
        NotificationCheckerInterface $notificationChecker
    )
    {
        $this->createPage = $createPage;
        $this->notificationChecker = $notificationChecker;
    }

    /**
     * @When I open the create new book page
     */
    public function iOpenTheCreateNewBookPage()
    {
        $this->createPage->open();
    }

    /**
     * @When I fill the book title with :title
     */
    public function iFillTheBookTitleWith($title)
    {
        $this->createPage->fillWithTitle($title);
    }

    /**
     * @When I add it
     */
    public function iAddIt()
    {
        $this->createPage->add();
    }

    /**
     * @Then I should be notified that new book was created
     */
    public function iShouldBeNotifiedThatNewBookWasCreated()
    {
        $this->notificationChecker->checkNotification('Book has been successfully created.', NotificationType::success());
    }
}

And that's it. We created basic Behat feature story with some steps with a definition of services that makes this feature testable. Now after running $ bin/behat, you should see some errors which is good. Remember crashing tests before implementation in TDD? In this case, BDD is no different.

Let's finally start with the proper implementation! ...of PHPSpec :)

Introducing Sylius BDD, 26 cze 2017 Sylius BDD - PHPSpec, 27 cze 2017