18 Mar 2015 · Software Engineering

    Integration testing PHP applications with Behat

    12 min read
    Contents

    Software testing is slowly becomming second nature for many developers. Knowing that your changes aren’t breaking any existing feature gives a huge confidence boost when deploying. The PHP community isn’t the first to adopt the methologies of TDD (Test-Driven Development) but it’s catching up quickly with the rest. The testing community is growing rapidly and many good testing tools and frameworks, like Behat, are born as a result.

    The first contact between your customers and product is through the web browser so it makes sense to start our testing from there. Automatic integration testing essentially consists of starting a browser which loads your app and executes steps based on requirements. If the requirement is met, the test passes, on the other hand it fails.

    It’s easy to dwell into your code for hours on end, but that’s also a good way to lose focus and forget about the main goal of the feature your working on. Taking a look at a feature from high above puts things into perspective and helps you stay on track. This is a nice benefit which comes with the practice of writing integration tests.

    The higher you are, the smaller the world below you is and eventually some things will become invisible. This, in our case, means that integration tests are testing only big features and aren’t considered about the details. Mike Cohn developed this concept with the test pyramid. The big down side of automatic integration testing is its speed. Imagine having to run your full test suite multiple times during a debugging session. That’s why there should be considerably less integration tests than unit tests.

    In this tutorial, we’ll create an example project with the Laravel PHP framework and run a couple of integration tests with the help of Behat and Mink.

    Setting up the example project

    The easiest way to start a new Laravel project is through Composer.

    Setting up Composer

    The installation of the package manager is quite simple. The command below is executed in my work directory /home/ervin/workspace and this is where the composer.phar will live.

    curl -sS https://getcomposer.org/installer | php

    This will download the latest Composer package and you can use it right away by calling php composer.phar. To avoid typing the whole command over and over again, create an alias. Naturally, the paths in your case will be different depending on your setup.

    which php # shows us where the bin direcotry of PHP is
    /home/ervin/.phpbrew/php/php-5.6.4/bin/php
    

    Next, create the file /home/ervin/.phpbrew/php/php-5.6.4/bin/composer containing the following code.

    #!/usr/bin/env bash
    
    "/home/ervin/.phpbrew/php/php-5.6.4/bin/php" "/home/ervin/workspace/composer.phar" $@
    

    The last step is to make the file executable and test out our alias.

    $ chmod +x /home/ervin/.phpbrew/php/php-5.6.4/bin/composer
    $ composer --version
    Composer version 1.0-dev
    

    If you see version information in your output, that means Composer is set up.

    Setting up Laravel

    Our next step is to create a blank Laravel project.

    The following command will install Laravel and its’ dependecies to the php-integration-testing directory which will be created automatically.

    composer create-project laravel/laravel --prefer-dist php-integration-testing

    In the rest of the article, we will add the Behat package to serve as an interpreter of the feature files (where our tests are defined) and Mink, to emulate and control the browser in which these tests will run.

    We’ll create a simple page which shows two greeting messages. The contents of app/views/hello.php is shown below.

    <!doctype html>
    <html lang="en">
    
    <body>
      <h1>Hello World!</h1>
      <span id="js-greeting"></span>
      <button onclick="delayedGreeting();">Say hello</button>
    </body>
    
    <script>
      function showGreeting(){
        document.getElementById("js-greeting").innerHTML = "Hello JavaScript!"
      }
    
      function delayedGreeting(){
        window.setTimeout(showGreeting, 1000);
      }
    </script>
    
    </html>

    The page initially shows “Hello World!” and when the “Say hello” button is pressed, the “Hello Javascript!” text is added after a 1 second delay. Our main goal is to see how our setup will handle dynamic and static content in the tests.

    Start the server in a separate terminal and leave it running because our tests will depend on it:

    $ php artisan serve
    Laravel development server started on http://localhost:8000
    

    Behat

    Behat is a BDD (behavior-driven development) framework which helps you to define features in human readable text written in a DSL (Domain Specific Language). Features are groupped in separate files which consist of the descriptions of the features along with scenarios which put the features in different situations.

    Behat uses Gherkin as its goto DSL. This specification is written after gathering enough information about each feature from your client. In this case, we are our own clients, so we can carry on.

    Setting up Behat

    Enter the example project’s directory.

    cd php-integration-testing

    We will use composer to install Behat to our project. Just include the package in the require block in composer.json like below.

    {
      "name": "laravel/laravel",
        "description": "The Laravel Framework.",
        "keywords": ["framework", "laravel"],
        "license": "MIT",
        "type": "project",
        "require": {
          "laravel/framework": "4.2.*",
          "behat/behat": "3.0.6",
          "behat/mink": "1.6.*",
          "behat/mink-extension": "*",
          "behat/mink-selenium2-driver": "*",
          "behat/mink-goutte-driver": "*"
    
        },
        "autoload": {
          ...
        }
    ...
    }
    

    Parts generated by the Laravel installation are left out from the output as we don’t need to make any changes there.

    After you save the file, run composer install or if you already ran that before, composer update. Now we have access to vendor/bin/behat.

    Initialize Behat and we’re ready to go:

    $ vendor/bin/behat --init
    +d features - place your *.feature files here
    +d features/bootstrap - place your context classes here
    +f features/bootstrap/FeatureContext.php - place your definitions, transformations and hooks here
    

    Defining a feature

    Let’s write our first feature which defines that we have to see a greeting message on the home page. The file we’re going to create is features/hello.feature.

    Feature: Greeting
      In order to see a greeting message
      As a website user
      I need to be able to view the homepage
    

    We briefly describe the feature with a title, followed by three lines of description which are not parsed by Behat. They serve only to provide context for the reader. We outline the goal of the feature, the user who is using it and the desired outcome.

    The next step is defining our first scenario.

    Feature: Greeting
    In order to see a greeting message
    As a website user
    I need to be able to view the homepage
    
      Scenario: Greeting on the home page
        Given I am on "/"
        Then I should see "Hello World!"
    

    After describing the scenario our feature is in, we follow a simple Given (context), When (event), Then (outcome) pattern with an ocassional And or But which are used to extend the previous statement. The quoted parts are matched with a regular expression, and they’re used as parameters in functions when we define our steps.

    With our first feature defined, we’re ready to set up the browser emulation and to implement the steps of the feature in PHP.

    Mink

    Imagine defining each step for the plethora of browsers out there whith vastly different capabilities and implementations. It doesn’t sound much fun does it? This is where Mink comes in. It’s a browser emulator which provides a consistent API regardless of the targeted browser.

    There are two types of browser emulators: browser controllers and headless browsers.

    Browser controllers fire up a real, full fledged web browser like [Firefox] (https://www.mozilla.org/en-US/firefox) and execute the steps in them. That’s visually really attractive, but performance wise, not so much.

    On the other hand headless browsers are fast, slim and easy to configure. They handle simple HTTP requests and parse their response.

    So who in the right mind would use browser controllers? Well, there’s a big disadvatage running headless. There’s no support for JS/AJAX. We have to choose between the performance of headless browsers and the JS support of browser controllers.

    It’s a tough choice but Mink comes to the rescue. It enables you to use the most suitable browser for the scenario. This means that our test suite can lunch Firefox to test features requireing JS and use a headless browser for testing non-dynamic pages. Truly the best of both worlds.

    Mink communicates with browsers through Drivers and supports 5 out of the box. Drivers are responsible for implementing the browser actions and providing them to Mink.

    Setting up Mink

    Adding Mink to your project is simple. Just extend your composer.json with the Mink package and the drivers you plan to use.

    {
      "name": "laravel/laravel",
        "description": "The Laravel Framework.",
        "keywords": ["framework", "laravel"],
        "license": "MIT",
        "type": "project",
        "require": {
          "laravel/framework": "4.2.*",
          "behat/behat": "3.0.6",
          "behat/mink": "1.6.*",
          "behat/mink-extension": "*",
          "behat/mink-selenium2-driver": "*",
          "behat/mink-goutte-driver": "*"
    
        },
        "autoload": {
          ...
        }
    ...
    }
    

    Running composer update will install the added packages. I’m using the Goutte (headless) and the [Selenium2] (http://seleniumhq.org/) (browser controller) drivers. [MinkExtension] (https://github.com/Behat/MinkExtension) is a really handy package which essentially glues together Behat and Mink. It adds services like Drivers, Sessions and Mink to Behat.

    The last thing is to provide a simple configuration file called behat.yaml in the project’s root directory, where we define our drivers for the MinkExtension sessions.

    default:
      extensions:
        Behat\MinkExtension:
          base_url: "http://localhost:8000"
          sessions:
            first_session:
              goutte: ~
            second_session:
              selenium2: ~
    

    The base_url defines what domain is loaded at the beginning of the run. We’ll use http://localhost:8000 as we’re testing our features on the local server. Sessions are used run scenarios with different drivers. If a session is not explicitly configured for the scenario in the feature definition, it defaults to the first headless browser or if the @javascript tag is present, the first browser supporting JS is used. This can also be [customized] (https://github.com/Behat/MinkExtension/blob/master/doc/index.rst#sessions).

    Setting up Selenium server

    The Selenium2 driver communicates with the Selenium Server which is responsible for running the browser. Just download [selenium-server] (http://selenium-release.storage.googleapis.com/2.44/selenium-server-standalone-2.44.0.jar) to your work directory, outside the project, and fire it up in a separate terminal.

    wget http://selenium-release.storage.googleapis.com/2.44/selenium-server-standalone-2.44.0.jar
    java -jar selenium-server-standalone-2.44.0.jar
    

    We’ll use Firefox to run our tests in. Having the latest versions installed of both Firefox and Selenium server will make sure that everything works as expected.

    Showtime

    When we initialized behat earlier, it generated the features/bootstrap/FeatureContext.php file where steps are defined. The only change we need to make it to extend the MinkContext class.

    <?php
    
    use Behat\MinkExtension\Context\MinkContext;
    use Behat\Behat\Context\SnippetAcceptingContext;
    
    class FeatureContext extends MinkContext implements SnippetAcceptingContext {  }
    

    This is the moment we’ve been waiting for. Run Behat from your project’s root folder.

    ./vendor/bin/behat
    

    You should be greeted with a passing test.

    1 scenario (1 passed)
    2 steps (2 passed)
    0m3.13s (15.36Mb)
    

    But wait. We haven’t defined any steps, which mirror our actions from features/greeting.feature. How does it know what to do? MinkExtension comes included with a few predefined steps which can be listed.

    $ /vendor/bin/behat -df
    
    default | Given I wait for the suggestion box to appear
    | at FeatureContext::iWaitForTheSuggestionBoxToAppear()
    
    default | When /^(?:|I )move forward one page$/
    | Moves forward one page in history
    | at FeatureContext::forward()
    
    default | When /^(?:|I )follow "(?P<link>(?:[^"]|\\")*)"$/
    | Clicks link with specified id|title|alt|text.
    | at FeatureContext::clickLink()
    
    default | When /^(?:|I )fill in "(?P<field>(?:[^"]|\\")*)" with "(?P<value>(?:[^"]|\\")*)"$/
    | Fills in form field with specified id|name|label|value.
    | at FeatureContext::fillField()]))]))]))"
    ...
    

    Testing the autocomplete feature

    Let’s add a test for the JavaScript message which is shown when the “Say hello” button is pressed. Add the following scenario to features/greeting.feature.

    Feature: Greeting
    In order to see a greeting message
    As a website user
    I need to be able to view the homepage
    
      Scenario: Greeting on the home page
        Given I am on "/"
        Then I should see "Hello World!"
    
        @javascript
        Scenario: Greeting in an alert box
          Given I am on "/"
          When I press "Say hello"
          And I wait for the greeting to appear
          Then I should see "Hello JavaScript!"
    

    You’ll notice that there’s a step which is not implemented yet: And I wait for the greeting to appear. This step has to be included in features/bootstrap/FeatureContext.php. We can do it manually but Behat has a trick up its sleeve.

    /vendor/bin/behat --dry-run --append-snippets
    

    The unimplemented feature’s skeleton will be added autoamtically to features/bootstrap/FeatureContext.php.

    Next, open the features/bootstrap/FeatureContext.php file, and replace throwing of the PendingException() with our expectation that there are suggestion displayed.

    As a side note, this auto-addition of snippets is the reason we’re implementing the SnippetAcceptingContext in the FeatureContext class.

    <?php
    
    use Behat\Behat\Tester\Exception\PendingException;
    use Behat\Behat\Context\SnippetAcceptingContext;
    use Behat\MinkExtension\Context\MinkContext;
    
    class FeatureContext extends MinkContext implements SnippetAcceptingContext {
      /**
       * @Given I wait for the greeting to appear
       */
      public function iWaitForTheGreetingToAppear()
      {
        $this->getSession()->wait(5000, "document.getElementById('js-greeting').innerHTML != ''");
      }
    }
    

    The function waits for the script to become true or timeouts after 5 seconds.

    Run Behat again and your web browser will start when the second scenario begins showing you all the actions it takes. Mission accomplished.

    $ ./vendor/bin/behat
    2 scenarios (2 passed)
    6 steps (6 passed)
    0m3.61s (14.94Mb)
    

    Wrapping up

    Using automatic integration tests gives you a safety net and assures that your users will always get what they’re signed up for. As a bonus, feature deifnitions can act as documentation too.

    Behat is a great tool but there are alternatives too. The biggest rival to Behat is Codeception which uses a sligthly different approach. We’ll cover Codeception in separate article.

    Here is some additional reading materials which describe the tools and methodologies mentioned here in more detail.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by: