Basic Usage

The best way of understanding how things work in detail is showing and analyzing examples, that is why this section gathers all the knowledge from the other chapters. Let’s assume that we are going to implement the functionality of managing reservations in our system.

Describing features

Let’s start with writing our feature file, which will contain answers to the most important questions: Why (benefit), who (actor using the feature) and what (the feature itself). It should also include scenarios, which serve as examples of how things supposed to work. features/user/managing_reservations/browsing_reservation.feature Scenario:

@managing_reservations
Feature: Browsing reservations
    In order to manage my reservation configuration
    As a commercial
    I want to be able to browse reservations

    Background:
        Given I am logged in as a commercial
        And I have access to read module "reservation"

    @ui @javascript
    Scenario: Browsing specific reservation
        Given the airline has a reservation
        And the airline has a commercial leg from airport "Hurghada" to airport "Alexandria" started at "3 days 06:17" on airplane "F-HERE"
        And this leg has 1 passenger
        And this leg flight on "commercial" flight type
        And this reservation contain this leg
        And the airline has another commercial leg from airport "Lugano" to airport "Petropavlovsk" started at "5 days 12:00" on airplane "F-HERE"
        And this leg has 2 passengers
        And this leg flight on "commercial" flight type
        And this reservation contain this leg
        When I want to browse reservations
        Then I should see 1 or more reservation in the list
        And this reservation should appear in airline

Pay attention to the form of these sentences. From the developer point of view they are hiding the details of the feature’s implementation. We are using sentences that are less connected with the implementation, but more focused on the effects of our actions. A side effect of such approach is that it results in steps being really generic, therefore if we want to add another way of testing this feature for instance in the domain or api context, it will be extremely easy to apply. We just need to add a different tag (in this case “@domain”) and of course implement the proper steps in the domain context of our system. To be more descriptive let’s imagine that we want to check if a reservation is created properly in two ways. First we are checking if the creation works via frontend, so we are implementing steps that are clicking, opening pages, filling fields on forms and similar, but also we want to check this action regardlessly of the frontend, for that we need the domain, which allows us to perform actions only on objects.

Choosing a correct suite

After we are done with a feature file, we have to create a new suite for it. At the beginning we have decided that it will be a frontend/user interface feature, that is why we are placing it in “Openjet/Bundle/AppBundle/Behat/Resources/config/suites/ui/user/managing_reservations.yml”.

default:
    suites:
        ui_managing_reservations:
            contexts_services:
                - openjet.behat.context.hook.doctrine_orm
                # This service is responsible for clearing database before each scenario,
                # so that only data from the current and its background is available.

                - openjet.behat.context.transform.datetime
                - openjet.behat.context.transform.shared_storage
                - openjet.behat.context.transform.airport
                - openjet.behat.context.transform.airplane
                - openjet.behat.context.transform.leg
                - openjet.behat.context.transform.flight_type
                # The transformer contexts services are responsible for all the transformations of data in steps.

                - openjet.behat.context.setup.reservation
                - openjet.behat.context.setup.leg
                - openjet.behat.context.setup.security
                # The setup context here is preparing the background, adding users or administrators.
                # These context have steps like "I am logged in as an administrator" already implemented.

                - openjet.behat.context.ui.sales.managing_reservations
                # These contexts are essential here we are placing all action steps.
            filters:
                tags: "@managing_reservations && @ui"

A very important thing that is done here is the configuration of tags, from now on Behat will be searching for all your features tagged with @managing_reservations and your scenarios tagged with @ui. Second thing is contexts_services:: in this section we will be placing all our services with step implementation.

We are almost finished with the suite configuration. Now we need to register our first Behat context as a service, but beforehand we need

Registering Pages

The page object approach allows us to hide all the detailed interaction with ui (html, javascript, css) inside.

We have three kinds of pages:
  • Page - First layer of our pages it knows how to interact with DOM objects. It has a method ->getUrl(array $urlParameters) where you can define a raw url to open it.
  • SymfonyPage - This page extends the Page. It has a router injected so that the ->getUrl() method generates a url from the route name which it gets from the ->getRouteName() method.
  • Base Crud Pages (IndexPage, CreatePage, UpdatePage) - These pages extend SymfonyPage and they are specific to the Openjet resources. They have a resource name injected and therefore they know about the route name.

There are two ways to manipulate UI - by using ->getDocument() or ->getElement('your_element'). First method will return a DocumentElement which represents an html structure of the currently opened page, second one is a bit more tricky because it uses the ->getDefinedElements() method and it will return a NodeElement which represents only the restricted html structure.

There is one small gap in this concept - PageObjects is not a concrete instance of the currently opened page, they only mimic its behaviour (dummy pages). This gap will be more understandable on the below code example.