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.