Testing with Behat and Mink

PHPUnit is really great for validating that the functions that you write behave as you expect them to. But they are only one part in the quest for a fully tested product.

If you are developing a web system, it can be a little tricky and time consuming to test the core functionalities every time you deploy. Automating the process can save time and ensure that you do not miss anything.

Today, I am going to document how you might go a step towards achieving this goal. The code in this demo can be found here.

The Components

There are several different components that all fit together to form a testing suite. Let's start by going through each of them.

Behat is quite simply described as "an open source Behavior-Driven Development framework for PHP".

Now I won't go into the details of Behavior-Driven Development (BDD) but Ciaran McNulty did an excellent talk covering this at the PHPUK Conference which you can watch here.

But basically its a type of acceptance testing where specifications for features are formally written and can be tested against. If all the boxes are ticked, then the feature is "done" or "working", if not, then we have to see why. Thats what Behat helps with.

Mink is an extension of Behat that is designed for testing in the browser. Behat on its own would be used like PHPUnit but with the tests written in English. It is Mink that lets you test the features in an end product.

Selenium is what Mink actually uses to actually connect to your website. It requires some extensions (which are covered in the setup setting below) in order to use specific browsers.

Setup

There are two steps to the setup.

First, get the Selenium server. You can get this from their website here. Choose the standalone server as this is the easiest for starting off with.

You also need to download the driver for the browser that you want to test in. Links to these can be found on the same Selenium page. But to get the Google chrome one, for example, just go here.

Once you have the driver downloaded, put it in a place such as usr/bin.

Next, run the server with the following command:

java -jar selenium-server-standalone-3.4.0.jar 

You can also specify the location of the browser drivers like so:

java -Dwebdriver.chrome.driver="/path/to/chromedriver" -jar selenium-server-standalone-3.4.0.jar 

But if you put it somewhere in your path variable, you shouldn't have to do this.

The second step is installing Behat and Mink. But this is attached to your actual project rather than a seperate standalone thing.

In your composer.json file, add the follow:

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

You can obviously change to versions that you desire. But as you can see, this installs Behat, Mink, the driver that connects the two together and the thing that connect Mink to the Selenium server.

Once you run the composer file, you will be done. We will see later how to run the tests.

Writing a Test

But first, lets write an actual test.

I have made a very simple application:

You simply type your name in, press submit and a message appears:

I want to write a test to test this functionality.

Folders and Files

First, lets get the folder structure right. This is completely configurable, but for simplicity of this blog, I am going to use a default structure.

Make a folder tests. Inside this, make a file called behat.yml. We will come back to that later.

Also in tests make a folder called features and inside this another called bootstrap.

Feature Test

Every feature has its own file. Inside this file, are a set of scenarios to test for that feature.

Inside the features folder, I made a new file, inputName.feature.

Inside this I have the following:

Feature: inputName
Scenario: User enters a lowercase name
        Given that I am on the first page
        When I input "antony" and submit the form
        Then I should see a message with "Antony"

To understand fully what is going on here, you should read this on the Behat website.

The language used in this is fundamental to Behaviour Driven Development. It is designed so that anyone in the business, not just the developers, can read and understand it. In an ideal world, they would also be the ones who put these together as a form of specifying features!

Contexts

Now we have to tell Behat how to run the above steps and this is where Mink comes in.

Inside the bootstrap folder, make a new file called MainContext.php.

Inside, make your class:

use Behat\MinkExtension\Context\MinkContext;

class MainContext extends MinkContext
{
    public function __construct()
    {
        require_once(realpath('../vendor/autoload.php'));
    }
}

As you can see, I am extending the MinkContext which is part of extension that I used earlier. This will make a whole bunch of functions available for me to use or override.

Now, for each line in the earlier test, you will want to make a function. You link the function to the associated line with a comment.

The first line was "Given that I am on the first page", so, I made this function:

/** @Given that I am on the first page */
public function onTheFirstPage()
{
    $this->visit('/');
}

You can see the block comment matches the line in the feature. This supports regular expressions and can do some very clever things.

One thing to watch out for are functions already declared in MinkContext that have the same comment, i.e. you have another function already declared for that line. You will be told about it when the test runs if this happens to you. You can then decide to use the existing implementation or you can adjust your instruction to avoid the conflict.

As you can see, this function does one thing: visit '/'. visit() is built into Mink and will go to any path relative to the root that you specify in the config (which we will do later). So in this case, I am going to the root. I believe that Mink already has a pattern for this that I could have used in my feature file to avoid re-implementing it (@Given /^(?:|I )am on (?:|the )homepage$/).

Now for the clever stuff.

The next line was, "When I input "antony" and submit the form".

So the function for that:

/** @When I input :name and submit the form */
public function inputNameSubmitForm($name)
{
    $this->fillField('my_name', $name);
    $this->pressButton('submit_name');
}

As you can see, the comment uses :name in place of "antony". This becomes a parameter in the function. It means I can reuse that line and put any word in that spot to test. In the function you can see a single parameter, $name, that matches the one in the comment.

The function makes use of two functions: fillField, which fills a given field with a value. The first parameter is the identifier of the field. Mink searches for fields by id, name, label and value in that order.

Then it does the pressButton. Funny thing about this is that I could not get it to work with the current version of the Firefox driver which is why I am doing this on Chrome. I am not sure if this is something to fix in the future.

The final line is "Then I should see a message with "Antony"". Again, I will make use of the ability to make a variable to test it:

/** @Then I should see a message with :name */
public function seeMessage($name)
{
    $page = $this->getSession()->getPage();
    $el = $page->find('named', ['id', 'greeting']);
if (!$el) {
        throw new \Exception ("Greeting not found.");
    }
    $match = preg_match('/Hello\, '.$name.'\!/', $el->getHtml());
    if ($match == 0) {
        throw new \Exception ("Unexpected greeting - ".$el->getHtml());
    }
}

Inside, you can see that I am using the find function on the page to get the element that contains the text. The first parameter tells it the type of find. In this case, I am using the name. I could use something like xpath instead. The second parameter is saying look for the id, "greeting".

The rest of the function is doing a simple regular expression check. Notice how I am throwing an exception to "fail" the test.

So that is our functions created. If you run the test without creating one of more functions for the lines in the feature, Behat will tell you what is missing and ask you if you would like it to auto-generate placeholders for it.

This is very helpful.

Bringing it together in the config

I left this step last so I could explain the different parts of the test. But I think you would probably do this step first. It is the config file.

Inside the root of the tests folder you should have a file called behat.yml. Inside add:

default:
    suites:
        default:
            contexts:
                - MainContext
    extensions:
        Behat\MinkExtension:
            base_url: http://localhost/behattest/
            sessions:
                default: 
                    selenium2: ~
            browser_name: chrome

So there are a few things going on here. As you can see, you can specify one or more suites to help organise your tests. In this case we are only declaring the default one.

Inside that, you can specify the name of your context classes. If you do not do this, it will search for the default one (called FeatureContext). When the test runs, it looks for the instruction in each of these classes, so you can organise code in these.

The next part declares what extensions are installed and some settings for them. In this case, we have only the MinkExtension. I have declared the base URL (which I mentioned earlier) and the fact that it should use the selenium2 server and Chrome. I can have multiple servers and different browsers of course.

Running the Test

Running the test is simple. Everything you need is installed in the vendor folder.

Simply run:

vendor/bin/behat --config /path/to/behat.yml

You should then see Google Chrome pop up and the tests run as if someone was using the browser! As this test is very short and simple, its finished a blink of an eye. But I can't wait to be able to watch entire suites run.

Summary

So there you have it. As usual, I have barely scratched the surface as to what can be achieved with Behat and Mink. Documentation for Behat can be found here, and Mink, here. I have also put this test on my Github so you can see it working as soon as you have installed everything.

Happy testing!


© 2012-2017