Symfony/BDD/Behat/Tutorial: Application 01: Rivers

4 January 2015

This is the tutorial that explains how to get started with: Symfony and Behat. It contains a step by step procedure to develop a simple web application in a BDD style.

1. Introduction

1.1. Background

I assume that you have already read (this is a compulsory reading):

You may find the following introductory posts interesting as they present smaller chunks of this tutorial:

1.2. The specification of the application

  1. The application to store rivers in the database.
  2. Every river has two attributes: name and length.
  3. Homepage should present an HTML table containing all the rivers from the database.
  4. Fixtures
    • load fixtures from YML file
    • show all the records from DB as an HTML table on homepage
  5. Access
    • Everyone can access the list of all the rivers in read mode.
    • Admin can access the list of all rivers in read/write mode through CRUD administration panel.
  6. Login/logout
    • login ,logged as X, logout - links/texts on homepage
    • homepage, panel admin - links on homepage
    • registration, resetting and profile - unvavailable
  7. The admin panel: CRUD
    • /admin/river/ - the administration panel available to logged in users
  8. Customized 404, 500 errors
  9. One user account available by default
    • username: admin
    • password: loremipsum

2. Introductory step

2.1. Start the project

Create a new directory and clone my Symfony Standard repository:

# Host OS
$ mkdir symfony-bdd-app-01-rivers
$ cd symfony-bdd-app-01-rivers
$ git clone git@github.com:by-examples/symfony-standard.git .

2.2. Create a starting point for the project

Create a new orphan branch that starts at the tag Symfony Standard v2.6.1:

# Host OS
$ git checkout --orphan 2.6.1/symfony-bdd-app-01-rivers v2.6.1

Commit your change:

# Host OS
$ git add -A
$ git commit -m "Symfony Standard 2.6.1"

2.3. Customize the project

Introduce the following changes in your project:

# Host OS
$ git cherry-pick origin/2.6.1/Gitignore
$ git cherry-pick origin/2.6.1/Cleanup
$ git cherry-pick origin/2.6.1/Vagrant
$ git cherry-pick origin/2.6.1/Speedup
$ git cherry-pick origin/2.6.1/Behat
$ git cherry-pick origin/2.6.1/BehatInitialization
$ git cherry-pick origin/2.6.1/Db
$ git cherry-pick origin/2.6.1/DoctrineFixturesBundle
$ git cherry-pick origin/2.6.1/FOSUserBundle
$ git cherry-pick origin/2.6.1/Error404
$ git cherry-pick origin/2.6.1/Travis
$ git cherry-pick origin/2.6.1/Scripts

2.4. Install the dependencies

Boot the VM with:

# Host OS
$ vagrant up
$ vagrant ssh

and run:

# Guest OS
$ composer install -o

When this is done, commit you changes with:

# Host OS
$ git add -A
$ git commit -m "Updated dependencies"

3. Run Behat - project is GREEN

Now, try to run:

# Guest OS
$ bin/behat

You will get the output similar to:

No scenarios
No steps
0m0.03s (9.00Mb)

The project is GREEN.

GREEN

4. Homepage

4.1. The feature file

Filename: homepage.feature

Feature: I would like to...

    Scenario: Homepage should be accessible
      Given I am on homepage
       Then I should see "The Longest Rivers in the World!"

The Changeset on GitHub

4.2. Behat - RED

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is RED.

RED

4.3. The commit - RED

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Homepage: test"

4.4. The code

Filename: app/Resources/views/base.html.twig:

...
<body>
    <h1>The Longest Rivers in the World!</h1>
    {% block body %}{% endblock %}
...

The Changeset on GitHub

4.5. Behat - GREEN

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is GREEN.

GREEN

4.6. The commit - GREEN

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] Homepage: code"

5. Error pages

5.1. The feature file

Filename: features/error.feature:

Feature: I would like to see customized error pages

    Scenario: The Page was not found!
      Given I am on "some/page/that/does/not/exist"
       Then the response status code should be 404
        And I should see "Hey, this beautiful page is yet to be created!"

    Scenario: Serious problem in the application
      Given I am on "/action/with/exception"
       Then the response status code should be 500
        And I should see "We are very sorry, but there was a serious error in the application!"

The Changeset on GitHub

5.2. Behat - RED

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is RED.

RED

5.3. The commit - RED

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Error pages: test"

5.4. The code

Filename: app/Resources/TwigBundle/views/Exception/error404.html.twig:

...
{% block body %}
    <h2>The page does not exists!</h2>
    <p>
        Hey, this beautiful page is yet to be created!
    </p>
{% endblock body %}
...

Filename: app/Resources/TwigBundle/views/Exception/error500.html.twig:

...
{% block body %}
    <h2>An error occured!</h2>
    <p>
        We are very sorry, but there was a serious error in the application!
    </p>
{% endblock body %}
...

Filename: src/AppBundle/Controller/DefaultController.php:

...
/**
 * @Route("/action/with/exception")
 */
public function actionWithExceptionAction()
{
    throw new \RuntimeException('Ups...');
}
...

The Changeset on GitHub

5.5. Behat - GREEN

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is GREEN.

GREEN

5.6. The commit - GREEN

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] Error pages: code"

6. Login/logout

6.1. The feature file

Filename: features/login.feature:

Feature: I would like to log in to the system

  Scenario: Log in as admin
    Given I am on homepage
     Then I should not see "Logged in as admin"
     When I follow "Login"
      And I fill in "username" with "admin"
      And I fill in "password" with "loremipsum"
      And I press "Login"
     Then I should see "Logged in as admin"
     Then I should not see "Login"
     When I follow "Logout"
     Then I should not see "Logged in as admin"
     Then I should see "Login"

  Scenario: Unsuccessful login
    Given I am on homepage
     When I follow "Login"
     When I fill in "username" with "wrong username"
      And I fill in "password" with "wrong password"
      And I press "Login"
     Then I should see "Bad credentials"

  Scenario: Profile unavailable
    Given I go to "/profile"
     Then the response status code should be 404

  Scenario: Resetting unavailable
    Given I go to "/resetting"
     Then the response status code should be 404

The Changeset on GitHub

6.2. Behat - RED

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is RED.

RED

6.3. The commit - RED

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Login/logout: test"

6.4. The code

Create the database and admin’s account:

# Guest OS
$ php app/console doctrine:schema:update --force
$ php app/console fos:user:create admin admin@example.net loremipsum --super-admin

Filename: app/Resources/views/base.html.twig:

...
<h1>The Longest Rivers in the World!</h1>
{% block login_logout_panel %}
    <ul>
        <li>
            <a href="{{ path('homepage') }}">
                Homepage
            </a>
        </li>
        {% if is_granted('ROLE_USER') %}
            <li>Logged in as {{ app.user.username }}</li>
            <li>
                <a href="{{ path('fos_user_security_logout') }}">
                    {{ 'layout.logout'|trans({}, 'FOSUserBundle') }}
                </a>
            </li>
        {% else %}
            <li>
                <a href="{{ path('fos_user_security_login') }}">
                    {{ 'layout.login'|trans({}, 'FOSUserBundle') }}
                </a>
            </li>
        {% endif %}
    </ul>
{% endblock login_logout_panel %}
{% block body %}{% endblock %}
...

Filename: app/Resources/TwigBundle/views/Exception/error404.html.twig:

...
{% endblock title %}

{% block login_logout_panel %}
{% endblock login_logout_panel %}

{% block body %}
...

Filename: app/Resources/TwigBundle/views/Exception/error500.html.twig:

...
{% endblock title %}

{% block login_logout_panel %}
{% endblock login_logout_panel %}

{% block body %}
...

The Changeset on GitHub

6.5. Behat - GREEN

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is GREEN.

GREEN

6.6. The commit - GREEN

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] Login/logout: code"

7. CRUD

7.1. The feature file

Filename: features/crud.feature:

Feature: I would like to edit rivers

  Scenario Outline: Insert records
   Given I am on homepage
     And I follow "Login"
     And I fill in "Username" with "admin"
     And I fill in "Password" with "loremipsum"
     And I press "Login"
     And I go to "/admin/river/"
    Then I should not see "<river>"
     And I follow "Create a new entry"
    Then I should see "River creation"
    When I fill in "Name" with "<river>"
     And I fill in "Length" with "<length>"
     And I press "Create"
    Then I should see "<river>"
     And I should see "<length>"

  Examples:
    | river          | length |
    | ABC RIV        | 7182   |
    | Vistula RIV    | 1234   |
    | The Thames RIV | 555    |


  Scenario Outline: Edit records
   Given I am on homepage
     And I follow "Login"
     And I fill in "Username" with "admin"
     And I fill in "Password" with "loremipsum"
     And I press "Login"
     And I go to "/admin/river/"
    Then I should not see "<new-river>"
    When I follow "<old-river>"
    Then I should see "<old-river>"
    When I follow "Edit"
     And I fill in "Name" with "<new-river>"
     And I fill in "Length" with "<new-length>"
     And I press "Update"
     And I follow "Back to the list"
    Then I should see "<new-river>"
     And I should see "<new-length>"
     And I should not see "<old-river>"

  Examples:
    | old-river     | new-river       | new-length |
    | Vistula RIV   | VI-stula RIV    | 9876       |
    | ABC RIV       | The NEW RIV     | 3333       |


  Scenario Outline: Delete records
   Given I am on homepage
     And I follow "Login"
     And I fill in "Username" with "admin"
     And I fill in "Password" with "loremipsum"
     And I press "Login"
     And I go to "/admin/river/"
    Then I should see "<river>"
    When I follow "<river>"
    Then I should see "<river>"
    When I press "Delete"
    Then I should not see "<river>"

  Examples:
    |  river         |
    | VI-stula RIV   |
    | The NEW RIV    |
    | The Thames RIV |

The Changeset on GitHub

7.2. Behat - RED

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is RED.

RED

7.3. The commit - RED

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] River CRUD: test"

7.4. The code

7.4.1. Generate River entity

Run the command:

$ php app/console doctrine:generate:entity

The entity’s name is:

AppBundle:River

Leave default settings for format: annotation and add two columns:

name     -   string 255
length   -   integer

When you are finished press enter (as many times as necessary).

The Changeset on GitHub

7.4.2. Commit

Commit your changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Create River entity"

7.4.3. The Database

Update the database:

# Guest OS
$ php app/console doctrine:schema:update --force

7.4.4. CRUD

Generate CRUD:

# Guest OS
$ php app/console doctrine:generate:crud

You will be asked a number of questions. The three of them are crucial for us:

The Changeset on GitHub

7.4.5. Commit

Commit your changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Default CRUD for River entity"

7.4.6. Customize the list of records

Filename: src/AppBundle/Resources/views/River/index.html.twig:

...
<tbody>
{% for entity in entities %}
    <tr>
        <td>{{ loop.index }}.</td>
        <td>
            <a href="{{ path('admin_river_show', { 'id': entity.id }) }}">
                {{ entity.name }}
            </a>
        </td>
        <td>{{ entity.length }}</td>
        <td>
...

The Changeset on GitHub

7.5. Behat - GREEN

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is GREEN.

GREEN

7.6. The commit - GREEN

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] River CRUD: customized view for index action"

8. Fixtures

8.1. The feature file

Filename: features/fixtures.feature:

Feature: We want one page with all the rivers loaded from YML file

  Scenario: List mountains
    Given I am on homepage
     Then I should see "The Nile"
      And I should see "1234"
     Then I should see "The Thames"
      And I should see "9876"
     Then I should see "Mississipi"
      And I should see "3434"

  Scenario: I want to check the number of records
    When I am on homepage
    Then I should see 3 "table tbody tr" elements

The Changeset on GitHub

8.2. Behat - RED

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ bin/behat

The project is RED.

RED

8.3. The commit - RED

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Fixtures: test"

8.4. The code

8.4.1. YML file

Filename: data/rivers.yml:

- { name: "The Nile", length: 1234 }
- { name: "The Thames", length: 9876 }
- { name: "Mississipi", length: 3434 }

8.4.2. The code to load fixtures

Filename: src/AppBundle/DataFixtures/ORM/LoadRivers.php:

<?php

namespace AppBundle\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;

use Symfony\Component\Yaml\Yaml;

use AppBundle\Entity\River;

class LoadRivers implements FixtureInterface
{
    /**
     * {@inheritDoc}
     */
    public function load(ObjectManager $manager)
    {

        $filename = __DIR__ . '/../../../../data/rivers.yml';
        $yml = Yaml::parse(file_get_contents($filename));
        foreach ($yml as $item) {
            $river = new River();
            $river->setName($item['name']);
            $river->setLength($item['length']);
            $manager->persist($river);
        }

        $manager->flush();
    }
}

The Changeset on GitHub

8.4.3. Load the fixtures into the database

Run the command:

# Guest OS
$ php app/console doctrine:fixture -n

8.4.4. Commit

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[RED] Fixtures: YML file and LoadRivers class"

8.4.5. Create the homepage

Filename: src/AppBundle/Controller/DefaultController.php:

...
/**
 * @Route("/", name="homepage")
 */
public function indexAction()
{
    $em = $this->getDoctrine()->getManager();

    $entities = $em->getRepository('AppBundle:River')->findAll();

    return $this->render(
        'default/index.html.twig',
        array(
            'entities' => $entities,
        )
    );
}
...

Filename: app/Resources/views/default/index.html.twig:

{% extends '::base.html.twig' %}
{% block body %}
    <table>
        <thead>
            <tr>
                <th>#</th>
                <th>Name</th>
                <th>Length</th>
            </tr>
        </thead>
        <tbody>
        {% for entity in entities %}
            <tr>
                <td>{{ loop.index }}</td>
                <td>{{ entity.name }}</td>
                <td>{{ entity.length }}</td>
            </tr>
        {% endfor %}
        </tbody>
    </table>
{% endblock %}

The Changeset on GitHub

8.5. Behat - GREEN

Run Behat:

# Guest OS
$ php app/console cache:clear --env=prod
$ php app/console cache:warmup --env=prod
$ mysql -u root < 00-extra/db/create-empty-database.sql
$ php app/console doctrine:schema:update --force
$ php app/console doctrine:fixtures:load -n
$ php app/console fos:user:create admin admin@example.net loremipsum --super-admin
$ bin/behat

The project is GREEN.

GREEN

8.6. The commit - GREEN

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] Fixtures: action/view for homepage"

9. Final tweaks

Filename: app/Resources/views/base.html.twig:

...
{% if is_granted('ROLE_USER') %}
    <li>Logged in as {{ app.user.username }}</li>
    <li>
        <a href="{{ path('admin_river') }}">
            Panel
        </a>
    </li>
    <li>
...

9.2. Script to reload database

Filename: reload.bash:

...
mysql -u root < 00-extra/db/create-empty-database.sql
php app/console doctrine:schema:update --force

php app/console doctrine:fixtures:load -n
php app/console fos:user:create admin admin@example.net loremipsum --super-admin

The Changeset on GitHub

9.3. The commit

Commit you changes with:

# Host OS
$ git add -A
$ git commit -m "[GREEN] Tweaks: menu option and reload.bash"

9.4. Run the tests

# Guest OS
$ ./reload.bash
$ bin/behat  --format progress

You may run the tests arbitrary number of times:

$ bin/behat  --format progress
$ bin/behat  --format progress
$ bin/behat  --format progress

10. Visit app with your browser

Run web browser and visit:

http://localhost:8880/
http://localhost:8880/app_dev.php/

11. Remove unnecessary commits

Remove unnecessary commits from your repository:

# Host OS
$ git remote rm origin
$ git branch -D 2.7
$ git tag | xargs git tag -d
$ git reflog expire --all --expire=now
$ git prune
$ git gc

Now your repository contains only the commits that you have authored working on this example.

12. Travis

Thanks to .travis.yml and reload.bash the example should run on Travis out of the box. All you have to do is to:

Here is the link to Travis output for the original example.

13. The Example

You will find the source code of the example on GitHub.

For the instruction how to run the example refer to README.md file.

Fork me on GitHub