Behat + Selenium2 / Webdriver

10 Aug 2012 in Tech

There's an easier way to integrate Mink/Selenium2. Read about it in this post

If you just want the code, you can find a repo over on GitHub

Having spent most of my afternoon piecing together bits of documentation and blog posts to get Behat and Selenium2 (WebDriver) playing nicely together, I thought I'd document the process incase I ever have to do it again.

Installing Behat

The easiest way to get Behat installed is to use Composer. I'd not really used Composer before this, but it's really easy to get up and running with.

Once you've installed Composer, create a composer.json file that contains the following:

json
{
"require": {
"behat/behat": ">=2.2.2",
"behat/mink": "1.4@stable",
"behat/mink-selenium2-driver": "*"
}
}

Once that completes, you should be able to run behat with vendor/behat/behat/bin/behat. That gets old pretty quickly though, so I like to run ln -s vendor/behat/behat/bin/behat behat to create an alias in the current directory. If you do the same, you'll be able to run behat just by typing ./behat

You should be ready to run ./behat --init and get your project started. This will create the features folder, along with FeatureContext.php. You'll also need a behat.yml file. Create this file in the same directory as composer.json with the following content:

yaml
default:
context:
class: "FeatureContext"

Setting up the context

The next thing you want to do is set up a Mink context for running your tests in. To do this, add the following to the constructor in features/bootstrap/FeatureContext.php. I like to keep my contexts separated, so I tend to create a new GUIContext for tests that require a browser.

php
$this->useContext('gui',
new GuiContext($parameters)
);

You'll also need to create a new file in features/bootstrap named GuiContext.php with the following content:

php
use Behat\Behat\Context\BehatContext;
use Behat\Mink\Mink,
Behat\Mink\Session,
Behat\Mink\Driver\Selenium2Driver;
use Selenium\Client as SeleniumClient;
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
class GuiContext extends BehatContext
{
public function __construct(array $parameters)
{
$mink = new Mink(array(
'selenium2' => new Session(new Selenium2Driver($parameters['wd_capabilities']['browser'], $parameters['wd_capabilities'], $parameters['wd_host'])),
));
$this->gui = $mink->getSession('selenium2');
}
}

We initialise a new Mink instance using the selenium2 driver. $parameters is populated by behat.yml which we'll get to in a minute.

behat.yml

Your behat.yml needs a few config options setting to make Mink work. Here's what mine looks like now:

yaml
default:
context:
class: "FeatureContext"
parameters:
wd_host: "http://192.168.56.101:4444/wd/hub"
wd_capabilities: { "browser": "firefox", "version": "14" }

Note that wd_host in this instance is the IP address of your Selenium Server hub that's currently running. Which brings us on to…

Selenium Server

NB: I'm very new to this so whilst this works for me, it might not be the most efficient way to do it

Now, I'm not sure how this would work for people that work on GUI machines, but I develop in a virtual machine so there's no way to run browsers on them. To get around this, I use Selenium Grid to run the tests remotely on my iMac.

Firstly, you'll want to download Selenium Server on all the machines involved.

You'll want to run the following command on the machine that's running the tests (the machine on which you run ./behat). This sets up a hub that can receive connections from a node reporting that there are browsers available to test with.

bash
java -jar selenium-server-standalone-2.25.0.jar -role hub

On the machine that you want to run the browser on, run the command:

bash
java -jar selenium-server-standalone-2.25.0.jar -role node -hub http://192.168.56.101:4444/grid/register -browser browserName=firefox,version=14,maxInstances=1

This starts up Selenium server in node mode. Node mode registers that you have available browsers and fires up browser windows to automate.

The IP address provided should be the IP address of the machine that you set up as a hub. The browser parameter is what browser will be used to run the test. Finally, I set maxInstances to 1 so that we only run one test at a time and my machine doesn't fall over.

Once you have both of those running, jump back to the directory that contains ./behat and run it. Give it 20 seconds or so, and in that time you should see a Firefox instance appear and then close. If you check your terminal, you should also see the following output:

user@host$ ./behat No scenarios No steps 0m0.01s

Your first feature

Let's build a simple feature to show how to use Mink+Selenium. Start by creating features/google.feature with the following content:

Feature: Visit Google and search Scenario: Run a search for Behat Given I'm on "http://google.com/?complete=0" And I search for "behat" Then I should see "Behat — BDD for PHP" as the first result

We use http://google.com/?complete=0 as the URL parameter disables instant search so we can test submitting a form.

Run ./behat and you should see some text saying that 3 steps are undefined, and that you can implement the definitions with the provided snippets. Copy and paste the snippets into GuiContext.php and let's get to work filling them out. (If you want some predefined steps, take a look at MinkExtension on GitHub, or just grab the definitions from here. I'm going to be writing them from scratch as it's handy to know how to do it.)

So, our first definition:

php
/**
* @Given /^I'm on "([^"]*)"$/
*/
public function imOn($arg1)
{
$this->gui->start();
$this->gui->visit($arg1);
$this->gui->wait(5000);
$this->gui->stop();
}

This definition starts a browser instance, navigates to whatever we passed into $arg1, waits 5 seconds then closes the browser. If for any reason your step fails, be sure to restart both Java processes (I have no idea why, but tests won't run until I do).

So, let's remove the wait and the stop from that definition, and move onto the next one, iSearchFor. This one's a bit more involved as we need to select the input box.

We start by getting the page that we've navigated to:

php
$page = $this->gui->getPage();

Next, we need to fill in the search box. To do this, we use a built in Mink method, page::fillField. Finally, we need to find the submit button and click on it. To do this, we can either use an XPath or a CSS selector. I personally prefer CSS as I know it better.

This should leave us with some code that looks similar to the following:

php
/**
* @Given /^I search for "([^"]*)"$/
*/
public function iSearchFor($arg1)
{
$page = $this->gui->getPage();
$page->fillField("lst-ib", $arg1);
$page->find("css", ".jsb input[name='btnK']")->click();
}

We fill the search box with ID lst-ib with the value that we passed in through the feature file. You can specify the input ID, name or label (see docs) to select a field to fill. Then, we use a CSS selector to select the "Google Search" button and tell Behat to click it.

Our steps should now look something like this:

php
/**
* @Given /^I'm on "([^"]*)"$/
*/
public function imOn($arg1)
{
$this->gui->start();
$this->gui->visit($arg1);
}
/**
* @Given /^I search for "([^"]*)"$/
*/
public function iSearchFor($arg1)
{
$page = $this->gui->getPage();
$page->fillField("lst-ib", $arg1);
$page->find("css", ".jsb input[name='btnK']")->click();
}

If you run ./behat now, you'll see Firefox appear and search Google for Behat. Finally, we want to verify the title of the first result. To do this, we need to complete our final step definition.

We start by getting the page instance again, before using a CSS selector to grab the title of the first result on the page. As far as I can tell, that selector is h3 a. As we're using find() and not findAll() only the first item is returned. Then, we use getText() to get the text content of the node. Finally, we use a PHPUnit assertation to make sure the text is what we're after before shutting down the browser. The entire step looks something like this:

php
/**
* @Then /^I should see "([^"]*)" as the first result$/
*/
public function iShouldSeeAsTheFirstResult($arg1)
{
$text = $this->gui->getPage()->find('css', "h3 a")->getText();
assertEquals($text, $arg1);
$this->gui->stop();
}

Running ./behat one more time will hopefully result in something close to the following:

user@host$ ./behat Feature: Visit Google and search Scenario: Run a search for Behat # features/google.feature:3 Given I'm on "http://google.com/?complete=0" # GuiContext::imOn() And I search for "behat" # GuiContext::iSearchFor() Then I should see "Behat — BDD for PHP" as the first result # GuiContext::iShouldSeeAsTheFirstResult() 1 scenario (1 passed) 3 steps (3 passed) 0m5.241s

There we have it, a passing test case. I'm sure there are lots of different Behat/Mink tricks that I'm missing or just couldn't get working such as elementTextContains(). I'd rather have working tests that aren't quite right than no tests though, and there's always time to fix them up as I learn new things.

Known Issues

By creating the Mink instance in the GuiContext constructor, it pops up a few more times than it should do when running tests. This can get annoying if you have a lot of non-GUI tests to run too. Short of instantiating the Mink instance in every Given block I have, I'm not sure how to go about fixing this. If you have any ideas, please let me know below.