Defining Reusable Actions - Step Definitions¶
Gherkin language provides a way to describe your application behavior in business readable language. But how do you test that the described behavior is actually implemented? Or that the application satisfies our business expectations described in feature scenarios? Behat provides a way to map 1-to-1 your scenario steps (actions) to actual PHP code called step definitions:
/**
* @When /^I do something with "([^"]*)"$/
*/
public function iDoSomethingWith($argument)
{
// do something with $argument
}
Definitions Home - FeatureContext Class¶
Step definitions are just normal PHP methods. They are instance methods in a special class called Feature Context. This class can be easily created by running behat with the --init option from your project’s directory:
$ behat --init
After you run this command, Behat will set up a features directory inside your project:
The newly created features/bootstrap/FeatureContext.php will have an initial context class to get you started:
# features/bootstrap/FeatureContext.php
<?php
use Behat\Behat\Context\ClosuredContextInterface,
Behat\Behat\Context\TranslatedContextInterface,
Behat\Behat\Context\BehatContext,
Behat\Behat\Exception\PendingException;
use Behat\Gherkin\Node\PyStringNode,
Behat\Gherkin\Node\TableNode;
class FeatureContext extends BehatContext
{
// Your definitions will live here
}
All step definitions and hooks necessary for behavior testing your project will be represented as methods inside this class.
Creating Your First Step Definition¶
The main goal for step definitions is to be executed when its matching step is run in Behat. But just because a method exists within FeatureContext doesn’t mean Behat can find it. Behat needs a way to check that a concrete class method is suitable for a concrete step in a scenario. Behat matches FeatureContext methods to step definitions using regular expression matching.
When Behat runs, it compares special lines of Gherkin from each scenario to the regular expressions bound to each method in FeatureContext. If the line of Gherkin satisfies a bound regular expression, its corresponding step definition is executed. It’s that simple!
Behat uses phpDoc annotations to bind regular expressions to FeatureContext methods:
/**
* @When /^I do something with "([^"]*)"$/
*/
public function someMethod($methodArgument) {}
Let’s take a closer look at this code:
- @When is a definition keyword. There are 3 supported keywords in annotations: @Given/@When/@Then. These three definition keywords are actually equivalent, but all three are available so that your step definition remains readable.
- The text after the keyword is the regular expression (e.g. /^I am in a directory "([^"]*)"$/).
- All search patterns in the regular expression (e.g. ([^"]*)) will become method arguments ($methodArgument).
Note
Notice the comment block starts with /**, and not the usual /*. This is important for Behat to be able to parse such comments as annotations!
As you probably noticed, this regular expression is quite general and its corresponding method will be called for steps that contain ... I do something with "...", including:
Given I do something with "string1"
When I do something with "some other string"
Then I do something with "smile :-)"
The only real difference between those steps in the eyes of Behat is the text inside double quotes. This text will be passed to its step’s corresponding method as an argument value. In the example above, FeatureContext::someMethod() will be called three times, each time with a different argument:
- ->someMethod( $methodArgument = 'string1' );.
- ->someMethod( $methodArgument = 'some other string' );.
- ->someMethod( $methodArgument = 'smile :-)' );.
Note
Regular expression can’t automatically determine the datatype of its matches. So all method arguments coming from step definitions are passed as strings. Even if your regular expression matches “500”, which could be considered an integer, “500” will be passed as a string argument to the step definition’s method.
This is not a feature or limitation of Behat, but rather the inherent way regular expression matching works. It is your responsibility to cast string arguments to integers, floats or booleans where applicable given the code you are testing.
Casting arguments to specific types can be accomplished using step argument transformations.
Note
Behat does not differentiate between step keywords when matching regular expressions to methods. So a step defined with @When could also be matched to @Given ..., @Then ..., @And ..., @But ..., etc.
Your step definitions can also define multiple arguments to pass to its matching FeatureContext method:
/**
* @When /^I do something with "([^"]*)" and with (\d+)$/
*/
public function someMethod($stringArgument, $numberArgument) {}
Definition Snippets¶
You now know how to write step definitions by hand, but writing all these method stubs, annotations and regular expressions by hands is tedious. Behat makes this routine task much easier and fun by generating definition snippets for you! Let’s pretend that you have this feature:
# features/example.feature
Feature:
Scenario:
Given some step with "string" argument
And number step with 23
Run this feature in Behat:
$ behat features/example.feature
Behat will provide auto-generated snippets for your steps:
It not only generates the proper definition annotation type (@Given), but also a regular expression with string ("([^"]+)") or number ((\d+)) capturing, method name (someStepWithArgument(), numberStepWith()) and arguments ($argument1), all based just on text of the step. Isn’t that cool?
The only thing left for you to do is to copy that method snippets into your FeatureContext class and provide a useful body for them!
Step Execution Result Types¶
Now you know how to map actual code to PHP code that will be executed. But how can you tell what exactly “failed” or “passed” when executing a step? And how does Behat actually check that a step executed properly?
For that we have step execution types. Behat differentiates between seven types of step execution results: “Successful Steps”, “Undefined Steps”, “Pending Steps”, “Failed Steps”, “Skipped Steps”, “Ambiguous Steps” and “Redundant Step Definitions”.
Let’s use our previously introduced feature for all the following examples:
# features/example.feature
Feature:
Scenario:
Given some step with "string" argument
And number step with 23
Successful Steps¶
When Behat finds a matching step definition it will execute it. If the definition method does not throw an Exception, the step is marked as successful (green). What you return from a definition method has no significance on the passing or failing status of the definition itself.
Let’s pretend our context class contains the code below:
# features/bootstrap/FeatureContext.php
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/** @Given /^some step with "([^"]*)" argument$/ */
public function someStepWithArgument($argument1)
{
}
/** @Given /^number step with (\d+)$/ */
public function numberStepWith($argument1)
{
}
}
When you run your feature, you’ll see all steps passed and are marked green:
Note
Passed steps are always marked as green if colors are supported by your console.
Tip
Install php5-posix on Linux, Mac OS or other Unix system to be able to see colorful Behat output.
Undefined Steps¶
When Behat cannot find a matching definition, the step are marked as undefined, and all subsequent steps in the scenario are skipped.
Let’s pretend we have an empty context class:
# features/bootstrap/FeatureContext.php
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
}
When you run your feature, you’ll get 2 undefined steps that are marked yellow:
Note
Undefined steps are always marked as yellow if colors are supported by your console.
Note
All steps following an undefined step are not executed, as the behavior following it is unpredictable. These steps are marked as skipped.
Tip
If you use the --strict option with Behat, undefined steps will cause Behat to exit with 1 code.
Pending Steps¶
When a definition method throws a Behat\Behat\Exception\PendingException exception, the step is marked as pending, reminding you that you have work to do.
Let’s pretend your FeatureContext looks like this:
<?php
use Behat\Behat\Context\BehatContext,
Behat\Behat\Exception\PendingException;
class FeatureContext extends BehatContext
{
/** @Given /^some step with "([^"]*)" argument$/ */
public function someStepWithArgument($argument1)
{
throw new PendingException('Do some string work');
}
/** @Given /^number step with (\d+)$/ */
public function numberStepWith($argument1)
{
throw new PendingException('Do some number work');
}
}
When you run your feature, you’ll get 1 pending step that is marked yellow:
Note
Pending steps are always marked as yellow if colors are supported by your console, because they are logically similar to undefined steps.
Note
All steps following a pending step are not executed, as the behavior following it is unpredictable. These steps are marked as skipped.
Tip
If you use --strict option with Behat, pending steps will cause Behat to exit with 1 code.
Failed Steps¶
When a definition method throws a generic Exception (not a PendingException) during execution, the step is marked as failed. Again, what you return from a definition does not affect the passing or failing of the step. Returning null or false will not cause a step definition to fail.
Let’s pretend that your FeatureContext has the following code:
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/** @Given /^some step with "([^"]*)" argument$/ */
public function someStepWithArgument($argument1)
{
throw new Exception('some exception');
}
/** @Given /^number step with (\d+)$/ */
public function numberStepWith($argument1)
{
}
}
When you run your feature, you’ll get 1 failing step that is marked red:
Note
Failed steps are always marked as red if colors are supported by your console.
Note
All steps following a failed step are not executed, as the behavior following it is unpredictable. These steps are marked as skipped.
Tip
If Behat finds a failed step during suite execution, it will exit with 1 code.
Tip
Behat does not come with its own assertion library, but you can use any proper assertion tool out there, as long as its failed assertions throw an exceptions. For example, if you’re familiar with PHPUnit, you can use its assertions in Behat:
# features/bootstrap/FeatureContext.php
<?php
use Behat\Behat\Context\BehatContext;
use Behat\Gherkin\Node\PyStringNode;
require_once 'PHPUnit/Autoload.php';
require_once 'PHPUnit/Framework/Assert/Functions.php';
class FeatureContext extends BehatContext
{
/**
* @Then /^I should get:$/
*/
public function iShouldGet(PyStringNode $string)
{
assertEquals($string->getRaw(), $this->output);
}
}
Tip
You can get exception stack trace with -v option provided to Behat:
$ behat features/example.feature -v
Skipped Steps¶
Steps that follow undefined, pending or failed steps are never executed, even if there is a matching definition. These steps are marked skipped:
Note
Skipped steps are always marked as cyan if colors are supported by your console.
Ambiguous Steps¶
When Behat finds two or more definitions that match a single step, this step is marked as ambiguous.
Consider your FeatureContext has following code:
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/** @Given /^.* step with .*$/ */
public function someStepWithArgument()
{
}
/** @Given /^number step with (\d+)$/ */
public function numberStepWith($argument1)
{
}
}
Running your feature with this context will result in:
Behat will not make a decision about which definition to execute. That’s your job! But as you can see, Behat will provide useful information to help you eliminate such problems.
Redundant Step Definitions¶
Behat will not let you define a step expression’s corresponding regular expression more than once. For example, look at the two @Given regular expressions defined in this feature context:
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/** @Given /^number step with (\d+)$/ */
public function workWithNumber($number1)
{
}
/** @Given /^number step with (\d+)$/ */
public function workDifferentlyWithNumber($number1)
{
}
}
Executing Behat with this feature context will result in a Redundant exception being thrown:
Step Argument Transformations¶
Step argument transformations allow you to abstract common operations performed on step definition arguments into reusable methods. In addition, these methods can be used to transform a normal string argument that was going to be used as an argument to a step definition method, into a more specific data type or object.
Each transformation method must return a new value. This value then replaces the original string value that was going to be used as an argument to a step definition method.
Transformation methods are defined using the same annotation style as step definition methods, but instead use the @Transform keyword, followed by a matching regular expression.
As a basic example, you can automatically cast all numeric arguments to integers with the following context class code:
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/**
* @Transform /^(\d+)$/
*/
public function castStringToNumber($string)
{
return intval($string);
}
/**
* @Then /^a user '([^']+)', should have (\d+) followers$/
*/
public function assertUserHasFollowers($name, $count)
{
if ('integer' !== gettype($count)) {
throw new Exception('Integer expected');
}
}
}
Let’s go a step further and create a transformation method that takes an incoming string argument and returns a specific object. In the following example, our transformation method will be passed a username, and the method will create and return a new User object:
<?php
use Behat\Behat\Context\BehatContext;
class FeatureContext extends BehatContext
{
/**
* @Transform /^user (.*)$/
*/
public function castUsernameToUser($username)
{
return new User($username);
}
/**
* @Then /^a '(user [^']+)', should have (\d+) followers$/
*/
public function assertUserHasFollowers(User $name, $count)
{
if ('integer' !== gettype($count)) {
throw new Exception('Integer expected');
}
}
}
Transforming Tables¶
Let’s pretend we have written the following feature:
# features/table.feature
Feature: Users
Scenario: Creating Users
Given the following users:
| name | followers |
| everzet | 147 |
| avalanche123 | 142 |
| kriswallsmith | 274 |
| fabpot | 962 |
And our FeatureContext class looks like this:
<?php
use Behat\Behat\Context\BehatContext;
use Behat\Gherkin\Node\TableNode;
class FeatureContext extends BehatContext
{
/**
* @Given /^the following users$/
*/
public function pushUsers(TableNode $usersTable)
{
$users = array();
foreach ($usersTable->getHash() as $userHash) {
$user = new User();
$user->setUsername($userHash['name']);
$user->setFollowersCount($userHash['followers']);
$users[] = $user;
}
// do something with $users
}
}
A table like this may be needed in a step testing the creation of the User objects themselves, and later used again to validate other parts of our codebase that depend on multiple User objects that already exist. In both cases, our transformation method can take our table of usernames and follower counts and build dummy User objects. By using a transformation method we have eliminated the need to duplicate the code that creates our User objects, and can instead rely on the transformation method each time this functionality is needed.
Transformations can also be used with tables. A table transformation is matched via a comma-delimited list of the column headers prefixed with table::
<?php
use Behat\Behat\Context\BehatContext;
use Behat\Gherkin\Node\TableNode;
class FeatureContext extends BehatContext
{
/**
* @Transform /^table:name,followers$/
*/
public function castUsersTable(TableNode $usersTable)
{
$users = array();
foreach ($usersTable->getHash() as $userHash) {
$user = new User();
$user->setUsername($userHash['name']);
$user->setFollowersCount($userHash['followers']);
$users[] = $user;
}
return $users;
}
/**
* @Given /^the following users$/
*/
public function pushUsers(array $users)
{
// do something with $users
}
/**
* @Then /^I expect the following users$/
*/
public function assertUsers(array $users)
{
// do something with $users
}
}
Note
Transformations are powerful and it is important to take care how you implement them. A mistake can often introduce strange and unexpected behavior.
Step Execution Chaining¶
Sometimes it might be useful to pass execution flow from one step to another. For example, if during step definition execution you found that it might be better to call another step to keep from duplicating code, you can just return a step imitator object (substep) from a definition method:
<?php
use Behat\Behat\Context\BehatContext,
Behat\Behat\Context\Step\Then;
use Behat\Gherkin\Node\TableNode;
class FeatureContext extends BehatContext
{
/**
* @Then /^(?:|I )should be on "(?P<page>[^"]+)"$/
*/
public function assertPageAddress($page)
{
// check, that $page is equal to current page
}
/**
* @Then /^the url should match "(?P<pattern>[^"]+)"$/
*/
public function assertUrlRegExp($pattern)
{
if (!preg_match('/^\/.*\/$/', $pattern)) {
return new Then("I should be on \"$pattern\"");
}
// do regex assertion
}
}
Notice that when we do not provide a regular expression to Then the url should match "..." in the step definition assertUrlRegExp(), it returns a new Behat\Behat\Context\Step\Then instance. When a step definition returns such object, it finds and executes the step definition that matches the step text provided as that object’s argument.
Tip
There are three predefined substep classes you can to use:
- Behat\Behat\Context\Step\Given
- Behat\Behat\Context\Step\When
- Behat\Behat\Context\Step\Then
These are the same steps used to annotate step definitions.
You can also return steps with multiline arguments. You can even pass in a table as an argument:
/**
* @Given /^I have the initial table$/
*/
public function table()
{
$table = new Behat\Gherkin\Node\TableNode(<<<TABLE
| username | password |
| everzet | 123456 |
TABLE
);
return new Given('I have users:', $table);
}
Note
Steps executed as a chain will throw an exception for all result types except for Successful. This means you’ll never get snippets out of steps, called only through execution chain!
As of 2.0.4, if you want to pass more than one step in an execution chain, just return an array of substep instances:
/**
* @Given /I entered "([^"]*)" and expect "([^"]*)"/
*/
public function complexStep($number, $result)
{
return array(
new Step\Given("I have entered \"$number\""),
new Step\When("I press +"),
new Step\Then("I should see \"$result\" on the screen")
);
}