Doing Behavior Driven Development in PHP with Behat
A few weeks ago I made the trek to Cleveland to attend the fantastic Cleveland Ruby Brigade meeting, which happens to be held at one of the coolest user group gathering spots in the country. Jeff Morgan spoke on the topic of Cucumber, a behavior driven development tool that allows developers to write software tests in plain English (and more than 20 other spoken languages). This opens up a world of possibilities for further involving non-programming members of any project in the development process, not to mention making the adoption of a test-first approach to software development even more compelling.
Although I regularly use Ruby on a variety of projects, the majority of my time is spent working with PHP. So, I wondered whether Cucumber was limited solely to the Ruby and Rails community. Further investigation indicates that Cucumber can in fact be used in conjunction with all mainstream languages, among them Java, Python, and you guessed it, PHP. Check out the Cucumber wiki for a complete listing of supported languages and related notes.
This investigation also turned up a native PHP BDD solution named Behat. Behat works in a manner quite similar to Cucumber, including using the very same spoken language test syntax (incidentally known as Gherkin). Although still a beta and not yet as capable as Cucumber, Behat provides PHP developers with the ability to take advantage of BDD using the familiar PHP environment and syntax.
Installing Behat
Behat takes advantage of several PHP 5.3.1-specific features (among them anonymous functions and namespaces), so you'll need to run PHP 5.3.1 or newer. When you have confirmed this setup, install Behat by adding its PEAR channel and installing the project:
%>pear channel-discover pear.everzet.com
%>pear install everzet/behat-beta
You can further enhance Behat's capabilities using PHPUnit's assertion features. So, in order to follow along with the examples provided in this tutorial, be sure to install PHPUnit. See my PHPBuilder.com article Use PHPUnit to Implement Unit Testing in Your PHP Development for more information.
When installed, you're ready to begin doing BDD with Behat!
Writing Your First Behat Test
The Behat wiki defines Gherkin as a "business readable, domain specific language that lets you describe the software's behavior without detailing how that behavior is implemented." You'll use Gherkin to describe application features and the corresponding expected behavior. Behat will use regular expressions to translate these natural language descriptions into PHP code, which is used to actually test the application, with the added bonus of generating much of this PHP code for you!
This approach makes it easy-- and even fun--to begin testing your application models. Consider an application that allows users to create an account and requires that the username consist of at least six characters. We can use Behat to create a scenario that spells out this requirement. Place the following feature and corresponding scenario into a file named username.feature
, saving it to a directory named features
within your project's root directory:
# language: en
Feature: Valid Username
As a website user
In order to create an account
The username must consist of at least six characters
Scenario: Provide valid username
Given I provide "jasong" as a username
When I retrieve the username
Then the username should be set to "jasong"
Next, run the behat
command from within your project's root directory. Doing so will result in Behat parsing the feature file and generating the PHP code that will be used to test your application:
%>behat
Feature: Valid Username
As a website user
In order to create an account
The username must consist of at least six characters
Scenario: Provide valid username # features/username.feature:7
Given I provide "jasong" as a username
When I retrieve the username
Then the username should be set to "jasong"
1 scenario (1 undefined)
3 steps (3 undefined)
0.069s
You can implement step definitions for undefined steps with these snippets:
$steps->Given('/^I provide "([^"]*)" as a username$/',
function($world, $arg1) {
throw new EverzetBehatExceptionPending();
});
$steps->When('/^I retrieve the username$/', function($world) {
throw new EverzetBehatExceptionPending();
});
$steps->Then('/^the username should be set to "([^"]*)"$/',
function($world, $arg1) {
throw new EverzetBehatExceptionPending();
});
Paste the three generated steps into a file named username.php
, and save this file to a directory named steps,
which resides in the features
directory. At this stage, your application directory will look like this:
features/
username.feature
steps/
username.php
After creating the username.php
file, run Behat again and you'll see a different outcome:
Feature: Valid Username
As a website user
In order to create an account
The username must consist of at least six characters
Scenario: Provide valid username # features/username.feature:7
Given I provide "jasong" as a username # features/steps/username.php:5
TODO: write pending definition
When I retrieve the username # features/steps/username.php:9
Then the username should be set to "jasong" # features/steps/username.php:13
1 scenario (1 pending)
3 steps (2 skipped, 1 pending)
0.068s
Behat is telling us that it's time to begin implementing the Account
model. Although the best practice is to first complete the test before writing any code, for the purposes of this exercise I want to provide you with a mental picture of what the Account
model class looks like. So, nd create a class named Account.php
and place it within a directory named models
:
class Account {
private $_username;
public function setUsername($username)
{
if (strlen($username) < 6)
{
throw new Exception("username is not valid");
} else
{
$this->_username = $username;
}
}
public function getUsername()
{
return $this->_username;
}
}
Because you'll need to use this Account
class and additionally various PHPUnit features within the tests, create a bootstrap file named env.php
, saving it to a directory called support
within the features
directory and adding the following contents:
account = new Account();
Implementing Your Behat Test
With the Account
model and bootstrap file in place, you can begin implementing the test, starting with the Given
step. In this step we will put the system into a known state, assigning the username via the setUsername()
method:
$steps->Given('/^I provide "([^"]*)" as a username$/', function($world, $arg1) {
$world->account->setUsername($arg1);
});
Run behat
again and you'll see that one test is now passing:
...
Scenario: Provide valid username # features/username.feature:7
Given I provide "jasong" as a username # features/steps/username.php:5
When I retrieve the username # features/steps/username.php:9
TODO: write pending definition
Then the username should be set to "jasong" # features/steps/username.php:13
1 scenario (1 pending)
3 steps (1 passed, 1 skipped, 1 pending)
0.080s
Continue in this fashion, completing the When
and Then
steps as demonstrated here:
$steps->Given('/^I provide "([^"]*)" as a username$/', function($world, $arg1) {
$world->account->setUsername($arg1);
});
$steps->When('/^I retrieve the username$/', function($world) {
$world->username = $world->account->getUsername();
});
$steps->Then('/^the username should be set to "([^"]*)"$/', function($world, $arg1) {
assertEquals($world->username, $arg1);
});
Run Behat again and you'll see that all three tests have passed:
...
Scenario: Provide valid username # features/username.feature:7
Given I provide "jasong" as a username # features/steps/username.php:5
When I retrieve the username # features/steps/username.php:9
Then the username should be set to "jasong" # features/steps/username.php:13
1 scenario (1 passed)
3 steps (3 passed)
0.130s
Admittedly, this is an exceedingly simple example, but it nonetheless provides you with a clear understanding of Behat's functionality. Be sure to check out the Behat website and GitHub project page for more details!