Behaviour Driven Development with Symfony, Behat and Mink

This post is about testing the ajax functionality on noodleDig that handles user generated content, using BDD. This allows creation of ‘features’ that can either be tested in a headless browser, or for javascript stuff, an actual browser.

Set up in Symfony

I went through the docs and set this up, using this guide. So, I added the relevant dependencies into my composer.json file…

"require": {
        ...
        "behat/symfony2-extension": "*",
        "behat/mink": "*",
        "behat/mink-extension": "*",
        "behat/mink-browserkit-driver": "*",
        "behat/mink-goutte-driver": "*",
        "behat/mink-selenium2-driver": "*"
    },

I then updated vendors and set up the testing environment in the ‘behat.yml’ file, like so…

default:
  paths:
    features:  features
    bootstrap: features/bootstrap
  extensions:
    Behat\Symfony2Extension\Extension:
      mink_driver: true
    Behat\MinkExtension\Extension:
      base_url: http://localhost/SkNd/web/app_dev.php
      default_session: selenium2
      javascript_session: selenium2
      goutte: ~
      selenium2: ~
  context:
    parameters:
      base_url: http://localhost/SkNd/web/app_dev.php
annotations:
  paths:
    features: features/annotations
closures:
  paths:
    features: features/closures

Since I’m mainly using this BDD stuff for ajax testing, I chose selenium2 as the default browser. This means that when running the tests, an actual browser opens and runs through the tests.

Creating the features

Because I wanted to only create features for a specfic bundle (at the moment), I ran the command…

php behat.phar src/SkNd/UserBundle/Features/add_mw_ugc.feature

This feature is to test the ajax components of adding user generated content to a memory wall (on noodleDig.com). So, for example, when a user clicks the ‘add content’ button, they get a form. The processing of the form is done using ajax (read about that here) and then the content is dynamically added to the page.

Standard unit tests can be written for what happens on the Symfony side when the ajax request is made, but these tests won’t be able to ascertain if the client-side stuff worked or not.

The command above creates a skeleton structure with a Features folder, a ‘context’ folder and the ‘.feature’ file that behat needs to run. Then, its down to adding features and specifying context where necessary.

Writing behaviours

The thing about BDD is that instead of defining specific units of functionality as in TDD, you define a behaviour. So, in the case of the adding ugc element on noodledig, one feature could be described as follows…

Feature: Add Memory Wall UGC
    In order to add ugc content
    as a website user
    I need the resulting response to be shown immediately on the wall

According to the behat docs, the feature definition should define the feature name, its benefit, role and the feature itself. Next, the various scenarios of this feature need to be defined.

Scenarios

Scenarios are used to define specific use cases. So, in my example of adding ugc, one scenario would be this…

   @javascript
    Scenario: ajax adding ugc with invalid title shows errors 
        Given I am logged in as "simon" "pword"
        When I am on "/memorywall/show/3/simons-awesome-wall"
        And I fill in "Title" with "t"
        When I press "Add it"
        When I wait for errors to show
        Then I should see "Something went wrong there"

Scenarios follow a common format –

  1. Scenario title
  2. Context
  3. Action
  4. Result

So, in this example, the title is immediately after the word ‘scenario,’ the context is a person logged in to the site using the username ‘simon’ and the password ‘pword,’ the action is filling out a form with invalid data, and the result is an error message appearing. The only other thing here is the @javascript hook. This tells behat and mink to run using a real browser using the selenium2 driver I defined earlier (read about that here).

This post is not going to discuss setting up selenium2, but its not that difficult. Just google it. Suffice it to say, when running scenarios that have the @javascript hook, if you’re using selenium2, it’ll need to be running the background before you run your tests.

java -jar selenium-server-*.jar

Once I ran the feature using the command…

$ bin\behat src\SkNd\UserBundle\features\add_mw_ugc.feature

It failed. Why? Because some of the actions I used aren’t native to behat and mink and can’t be interpreted. Things like ‘press,’ ‘on,’ ‘fill in’ etc can be interpreted as actual commands by behat. But stuff like ‘wait for,’ ‘logged in as,’ can’t be interpreted. So, you have to tell behat how to handle these.

Feature Context

For the purposes of my features, I needed to hook into the Symfony extension so I could access stuff like the DIC. So, in the ‘FeatureContext.php’ class, I used the following libraries…

use Symfony\Component\HttpKernel\KernelInterface;
use Behat\Symfony2Extension\Context\KernelAwareInterface;
use Behat\MinkExtension\Context\MinkContext;

My class definition was then updated to be…

class FeatureContext extends MinkContext implements KernelAwareInterface

Then, its down to writing the methods that behat can use. I’ll take one of the methods thats a bit more involved…

The ‘given’ context

In the scenario above, there is a ‘given’ statement, which acts like a pre-condition. In the actual scenario, its saying that to add ugc, the user must be logged in. This pre-condition allows us to perform some processing before anything else happens. So, in the scenario of adding invalid ugc, the person has to log in first before they can do anything.

... 
Given I am logged in as "simon" "pword"
...

For behat to understand this, the following method needs creating…

/**
 * @Given /^I am logged in as "([^"]*)" "([^"]*)"$/
 */
public function iAmLoggedInAs($username, $password){
    $this->getSession()->visit($this->locatePath('/login'));
    $this->fillField('username', $username);
    $this->fillField('password', $password);
    $this->pressButton('Login');
}

For this method to work, 2 parameters must be passed to it (username and password). The annotation at the top of the method is important. Its giving the regular expression for the given statement. The two "([^"]*)" bits allow parameters to be passed from the .feature file, when they are in double quotes (more about that here).

Inside the method is code to visit the login page, and try to log in using the params supplied.

Conclusions

BDD seems to be a great way to test client-side ajax stuff and hook it into Symfony. I’ve not tried it for testing non-javascript type stuff, because I’m doing unit tests for all of that. However, to create readable and logical tests based on features rather than specific elements of functionality, it really makes sense.

If there are errors or areas for improvement (I’m sure there are), please leave some feedback.

Leave a Reply