Symfony2: Testing with Behat and Mink

I have been looking at functional testing using Behat and Mink and their associated Symfony2 bundles. Having used Selenium for this sort of testing in the past and found the writing of the tests to be a long and torturous process, Behat is a huge improvement.

There are quite articles about on getting started with Behat for web application testing as well as its own great documentation. For example, http://www.whiteoctober.co.uk/blog/2011/06/15/getting-into-behat-with-symfony2/ and http://techportal.ibuildings.com/2011/07/27/behaviour-driven-development-in-php-with-behat/.

I am not going to go over installation and the basics in this post but just look a couple of specific things I have found useful myself.

Mink's Built In Steps

One of the most useful features for me has been the built in steps provided when using Mink with Behat. These are available when you extend MinkContext for the FeatureContext class and allow you to write steps to perform common interactions with a website. For example clicking on a link:

When I follow "Edit your contact details"

These already have the behind the scenes action written so you do not need to write them yourself. There are numerous actions and assertions included. You can find a list of these with this command:

php app/console -e=test behat @ExampleBundle --definitions

As well as really appreciating not having to write the code for actually controlling the browser session for these I liked the way that they match to multiple ways of selecting a link, button, field etc. So instead of having to know the id of a field to fill it out, it will match to the following its id, name, label or value.

Another couple of useful steps included are great for finding why a feature is not passing:

Then print last response

Then show last response

Adding these to a feature will allow you to see the last output from the browser which helps to find out what wrong with a test. Then print last response will display the last output in stdout. To use Then show last response you need to add the following to the Mink config:

mink:
    #--
    show_cmd: firefox %s //or browser of your choice

This will then open the last response in the specified browser, which can be more helpful as you will see the rendered page and not the page source.

Adding users using the FOSUserBundle.

Here is a quick snippet for adding users if you are using the FOSUserBundle. If you have a Given step, effectively a precondition for a test, that requires certain users to be added e.g.

Given there are users:
      | username    | password | email                   |
      | testuser    | secret   | info@limethinking.co.uk |
      | anotheruser | password | info@limethinking.co.uk |

Then this step in the FeatureContext will add them:

    /**
     * @Given /^there are users:$/
     */
    public function thereAreUsers(TableNode $table)
    {
        $userManager = $this->getContainer()->get('fos_user.user_manager');
        foreach ($table->getHash() as $hash) {
            $user = $userManager->createUser();
            $user->setUsername($hash['username']);
            $user->setPlainPassword($hash['password']);
            $user->setEmail($hash['email']);
            $user->setEnabled(true);
            $userManager->updateUser($user);
        }
    }

Cleaning the database

The documentation's recommendation is not to use Doctrine fixtures for testing but to do this work in the FeatureContext class in order to make it easy to see the starting state from the scenario. You can clear database tables like this:

Given the database is clean
    /**
     * @Given /^the database is clean$/
     */
    public function theDatabaseIsClean()
    {
        $em = $this->getContainer()->get('doctrine.orm.entity_manager');
        $em->createQuery('DELETE LTSpringBundle:Testimonial')->execute();
        $em->createQuery('DELETE LTSpringBundle:Project')->execute();
        $em->flush();
    }

In the above code you are using an Entity with the Entity Manager to clear the database. If you are using Symfony2's ACL for security then this also needs clearing but does not have associated entities, the ACL tables can still be cleared by using the DBAL connection rather than the entity manager like this:

    /**
     * @Given /^the database is clean$/
     */
    public function theDatabaseIsClean()
    {
        //--

        $conn = $this->getContainer()->get('database_connection');
        $conn->executeQuery('DELETE FROM acl_classes');
        $conn->executeQuery('DELETE FROM acl_entries ');
        $conn->executeQuery('DELETE FROM acl_object_identities');
        $conn->executeQuery('DELETE FROM acl_object_identity_ancestors');
        $conn->executeQuery('DELETE FROM acl_security_identities');
    }