Intro to Behat & Selenium

Setting up behat with Selenium can be quite a task. I myself never actually got behat 3 to play nice with laravel and my setup so im still on behat 2. This is the first article about Behat testing but, depending on if this is helpful for people, there could possibly be a few more that illustrate how to get down a complete work flow.

Quick Recap

If you aren't familiar with the behat's friends ( mink and selenium or you are a bit new to behat ) Behat is a testing framework for translating the requirements of the business into actual tests.

It's friend Goutte is a package that makes web requests and scrapes content. It is ran in the console much like curl can be. Behat and Goutte are great for writing some tests but when you need to see how your app really works and how a user intereacts with it where do you turn?

The answer is Mink and Selenium. Mink is a wrapper around whatever framework you are using to simulate a browser experience. In other words, just like jQuery lets you use $.ajax to unify your ajax code instead of coding it multiple ways for multiple browsers, Mink lets you use a common interface to control the browser behavior.

Selenium is one driver you can use with Mink that will let you control a real instance of firefox.

Setting up Behat

The technical landscape we will be working with is this:

  • Laravel 4.1.*
  • behat
  • mink
  • Goutte
  • Selenium
  • Vagrant

Figuring out how to set this up was a bit tricky for a few reasons. First, obviously Laravel and everything runs from inside vagrant. Vagrant doesnt have a GUI, one that you typically use anyway. So how does one run Selenium ( Which uses firefox by default ) to see tests run and simulate a browser?

I saw a few posts that use some trickery to run firefox in memory or to port the actual app firing from vagrant into your native machine and use firefox there, one even installed firefox using apt-get. These are interesting
possibilities but for me overly complicated. So to get started download selenium server and put it anywhere on your machine. You'll also need firefox installed.

My solution for Vagrant and Selenium

If you are running vagrant with a mirrored file structure then you have the ability to run your selenium jar from your local machine where it can access firefox. So I did just that. I made a selenium folder in my project root. I then installed my other deps in vagrant through compser. Here's my require-dev settings:

"require-dev": {
        ......

        "behat/symfony2-extension": "*",
        "behat/behat": "2.5.*@stable",
        "behat/mink": "1.5.*@stable",
        "behat/mink-extension": "*",
        "behat/mink-goutte-driver": "*",
        "behat/mink-selenium2-driver": "*",
        "behat/mink-browserkit-driver": "*"
    },

Not sure if you actually need the symfony2 extension. Was planning on removing it and testing but havent gotten a chance.

Initialize Behat

So after your composer install finishes you need to create a behat.yml file to specify your behat preferences. Mine looks like this:

default:  
  paths:
    features: app/tests/behat
  extensions:
      Behat\MinkExtension\Extension:
          default_session: 'selenium2'
          base_url: http://dev.myapp.com
          browser_name: 'firefox'
          goutte: ~
          javascript_session: selenium2

          selenium2:
              wd_host: 127.0.0.1:4444/wd/hub
              capabilities: { "browser": "firefox", "version": "ANY", "selenium-version": "ANY", "deviceType": "ANY"}

Once this is done you can run your behat --init which will make your features directory and setup your basic files that are needed.

Next open up your behat/bootstrap/FeatureContext.php, or features/bootstrap/FeatureContext.php if you used the default folder name. Here we need to make sure your FeatureContext extends from the MinkContext instead of the default so change your class definition.

Next, make sure you have the correct namespaces:

use Behat\Behat\Context\ClosuredContextInterface,  
    Behat\Behat\Context\TranslatedContextInterface,
    Behat\Behat\Context\BehatContext,
    Behat\Behat\Exception\PendingException;

use Behat\Behat\Event\SuiteEvent,  
    Behat\Behat\Event\ScenarioEvent;

use Behat\Gherkin\Node\PyStringNode,  
    Behat\Gherkin\Node\TableNode;

use Behat\Behat\Hook\Scope\BeforeFeatureScope;  
use Behat\Behat\Hook\Scope\AfterFeatureScope;

use Behat\Mink\Driver\Selenium2Driver;  
use Behat\MinkExtension\Context\MinkContext;  
use Behat\Mink\Session;  
use Behat\Mink\Driver\DriverInterface;


class FeatureContext extends MinkContext {  
     //....

Alrighty, now its time to test. Off to the races!

Running the tests

Ok so to run the tests, on your native machine, bootup selenium by running java -jar selenium/selenium-server-standalone-2.39.0.jar where the selenium version is whichever one you are using.

Note when selenium boots up it tells you your URL for your wd_host param in your behat.yml file.

Now that selenium is running open a new tab and write your first behat scenario's.

Here's an excerpt from mine to get you going:

Feature: Home Page  
  In order to navigate the site
  As a visitor
  I should see Get Paid for Having FUN!! on the home page

  @javascript
  Scenario: User visits the home page
      Given I go to "/"
      When  I am on the "home" page
      Then  I should see "Get Paid for Having FUN!!"

  @javascript
  Scenario: I can advance to the screener page
      Given I go to "#categories"
      When  I am on the "categories" page
      Then  I should see "Topic Categories"

To use selenium preface your tests you want browser emulation for with @javascript

Now when you "go to" or when you write a "should be on" you will now see firefox actually taking you places. So this is the "front end" of the tests so to speak. Behind the scenes here is the code ( FeatureContext.php ) that goes with it:

use behat\TestHelper;  
use Behat\Behat\Context\ClosuredContextInterface,  
    Behat\Behat\Context\TranslatedContextInterface,
    Behat\Behat\Context\BehatContext,
    Behat\Behat\Exception\PendingException;

use Behat\Behat\Event\SuiteEvent,  
    Behat\Behat\Event\ScenarioEvent;

use Behat\Gherkin\Node\PyStringNode,  
    Behat\Gherkin\Node\TableNode;

use Behat\Behat\Hook\Scope\BeforeFeatureScope;  
use Behat\Behat\Hook\Scope\AfterFeatureScope;

use Behat\Mink\Driver\Selenium2Driver;  
use Behat\MinkExtension\Context\MinkContext;  
use Behat\Mink\Session;  
use Behat\Mink\Driver\DriverInterface;

class FeatureContext extends MinkContext  
{
    protected $session;
    protected $page;

    //array('key', 'location')
    protected $pageDictionary = array(
      'home' => array('/', '/'),
      'categories' => array('categories', '#categories')
    );


    public function __construct(array $parameters){}

    /** @BeforeScenario */
    public function before($scope)
    {
        $this->session  = $this->getMink()->getSession();
        $this->page     = $this->session->getPage();
    }

    /** @AfterScenario */
    public function after($scope)
    {
        unset($this->session);
        $this->session = null;
    }

    /**
     * @When /^I am on the "([^"]*)" page$/
     */
    public function iAmOnThePage($page_name)
    {
        $url     = $this->session->getCurrentUrl();
        $this->session->wait(2000);

        $onHome = TestHelper::isHomePage($page_name);
        if($onHome) $page_name = $url;
        $this->runPageTest($page_name);
    }

    public function runPageTest($page_key){
        $onHome = TestHelper::isHomePage($page_key);
        $this->session->wait(4000);

        //run the test for the page using the interpolated name
        if(! $onHome){
            $method = 'on'.ucfirst($page_key)."Page";
            //onCategoriesPage
            $this->$method();
        }
    }

    /** -- Page Methods ------------------------------------------------------------------------------------------- */
    public function onCategoriesPage(){
        throw new PendingException();
    }
}

Since I am using backbone routing on the frontend of my app I have been adding some helper methods and a bit of sugar on top of the standard methods. Note the stuff above is definately a WIP.

If you have a better way or i've done something wrong feel free to chime in in the comments or tweet me.

That should do it for the basics of setting this stuff up! Good luck.