First steps with Behat
Benjamin Grandfond10 min read
Warning: This blog post uses Behat 2.5. It is not all compatible with the ~3.0 version which should be released on 20th of April 2014.
At the beginning of the year, I decided it was time to give a try to BDD. Hence every new project I started from then on was done with BDD.
I found at the time lots of documentation/tutorials on the subject, but none was exactly what I was hoping for when I began: a standalone step-by-step tutorial that goes from installation to having tested a full user story through BDD, code samples included.
To help others get started BDD with Behat, I want to share with you such a basic tutorial. We will develop a simple calculator that takes an operation (like “1+1”) and returns a result on a web page. At the end of this post, you will have a functional calculator and you should know basics to start using Behat in your projects.
Setup the project
First of all, we need to install dependencies in our project, we’ll use composer to do so:
#composer.json
{
"autoload": {
"psr-0": { "Calculator": "src/" }
},
"require": {
"behat/behat": "*",
"behat/mink-extension": "*",
"behat/mink-goutte-driver": "*"
},
"minimum-stability": "dev",
"config": {
"bin-dir": "bin"
}
}
Then install Composer (“curl -s https://getcomposer.org/installer | php”) and run “php composer.phar install”. Note that we also installed Mink and Goutte, that will allow us to easily test our web application.
Your first feature
A feature is a file that describes scenarios step by step.
To initialize our suite of features run the following command:
~/calculator $ bin/behat --init
+d features - place your *.feature files here
+d features/bootstrap - place bootstrap scripts and static files here
+f features/bootstrap/FeatureContext.php - place your feature related code here
Once the “features” folder is created you can start writing your features. Create a file “calculator feature” in your “features” dir:
#features/calculator.feature
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2
Given I am on "/index.php"
When I fill in "operation" with "1+1"
And I press "Ok"
Then I should see "2" in the "#result" element
This needs some explanations. The first line contains the name of the feature after the keyword “Feature”
Then we wrote the first scenario of the feature right after the ‘Scenario’ keyword and gave it a name. As in this example, you can add a description of your scenario. Then we added the steps of the scenario describing what happens. Each step is prefixed by a keyword “Given”, “When”, “And” and “Then”, this allows the scenario to be readable by both a human being and Behat.
Ok, once the feature is written, we want to run behat to see the scenario fail:
~/calculator $ bin/behat
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2 # features/calculator.feature:3
Given I am on "/index.php"
When I fill in "operation" with "1+1"
And I press "Ok"
Then I should see "2" in the "#result" element
1 scenario (1 undefined)
4 steps (4 undefined)
0m0.015s
You can implement step definitions for undefined steps with these snippets:
/**
* @Given /^I am on "([^"]*)"$/
*/
public function iAmOn($arg1)
{
throw new PendingException();
}
/**
* @When /^I fill in "([^"]*)" with "([^"]*)"$/
*/
public function iFillInWith($arg1, $arg2)
{
throw new PendingException();
}
/**
* @Given /^I press "([^"]*)"$/
*/
public function iPress($arg1)
{
throw new PendingException();
}
/**
* @Then /^I should see "([^"]*)" in the "([^"]*)" element$/
*/
public function iShouldSeeInTheElement($arg1, $arg2)
{
throw new PendingException();
}
Behat told you a lot of interesting stuff… let’s decompose it. First, it tells you that he cannot understand so far the step we gave it: you need to define what “I am on” means, for instance.
Then it generates templates to help you write your step definitions. Most of the time you will copy/paste these templates in your context class generated in the feature/bootstrap/FeatureContext.php file. Luckily, MinkExtension provides these definitions with the MinkContext class, so lets use it.
Before going further we will have to configure Behat through the behat.yml file:
# behat.yml
default:
extensions:
Behat\MinkExtension\Extension:
base_url: 'http://127.0.0.1:4042' # this will be the url of our application
goutte: ~
Then register the MinkContext as a subcontext in the FeatureContext class that Behat generated for you:
// features/bootstrap/FeatureContext.php
...
use Behat\MinkExtension\Context\MinkContext;
/**
* Features context.
*/
class FeatureContext extends BehatContext
{
/**
* Initializes context.
* Every scenario gets it's own context object.
*
* @param array $parameters context parameters (set them up through behat.yml)
*/
public function __construct(array $parameters)
{
$this->useContext('mink', new MinkContext());
}
}
Now if we run “bin/behat -dl” we will see the list of step definitions that our context is aware of.
Then running “bin/behat” you should see the first step fail:
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2 # features/calculator.feature:3
Given I am on "/index.php" # Behat\MinkExtension\Context\MinkContext::visit()
[curl] 7: couldn't connect to host [url] http://127.0.0.1:4042/index.php
When I fill in "operation" with "1+1" # Behat\MinkExtension\Context\MinkContext::fillField()
And I press "Ok" # Behat\MinkExtension\Context\MinkContext::pressButton()
Then I should see "2" in the "#result" element # Behat\MinkExtension\Context\MinkContext::assertElementContainsText()
1 scenario (1 failed)
4 steps (3 skipped, 1 failed)
0m0.031s
To make the first step pass we have to make the “/index.php” url point to a real index.php file. Then you have to make Behat (throught Mink and Goutte) able to access this file.
To do so you can set up a virtual host, but if you don’t want to spend most of your time setting it up you should open another tab in your console, download Symfttpd and add the following symfttpd.conf.php file in your project:
<?php
// symfttpd.conf.php
$options['project_type'] = 'php';
$options['project_web_dir'] = '.';
Note that you will need Lighttpd to be installed on your machine (to use Nginx with PHP-FPM run the command “path/to/symfttpd/bin/symfttpd init”).
Once Symfttpd is configured, run the following command:
~/calculator $ php symfttpd.phar spawn -t
Symfttpd - version 2.1.4
lighttpd started on 127.0.0.1, port 4042.
Available applications:
http://127.0.0.1:4042/index.php
Press Ctrl+C to stop serving.
If everything is going right you can now run the scenario again:
~/calculator $ bin/behat
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2 # features/calculator.feature:3
Given I am on "/index.php" # Behat\MinkExtension\Context\MinkContext::visit()
When I fill in "operation" with "1+1" # Behat\MinkExtension\Context\MinkContext::fillField()
Form field with id|name|label|value "operation" not found.
And I press "Ok" # Behat\MinkExtension\Context\MinkContext::pressButton()
Then I should see "2" in the "#result" element # Behat\MinkExtension\Context\MinkContext::assertElementContainsText()
1 scenario (1 failed)
4 steps (1 passed, 2 skipped, 1 failed)
0m1.154s
Now the first step is green, this means it passed, but the second one failed: “Form field with id|name label|value “operation” not found.” It means that we have some minimal code to write.
Green bar
In TDD we follow simple rules:
- write a simple test
- run all tests and fail
- make a change
- run the tests and succeed
- finally refactor
The first step is already done since we wrote our scenario, and as we ran it and saw it fail step two is already done as well. Let’s make a change then.
To make the scenario succeed we need to write some html in the index.php file:
<html>
<body>
<form action="/">method="post">
<input type="text">name="operation" />
<button type="submit">Ok</button>
</form>
</body>
</html>
Then run Behat again:
~/calculator $ bin/behat
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2 # features/calculator.feature:3
Given I am on "/" # Behat\MinkExtension\Context\MinkContext::visit()
When I fill in "operation" with "1+1" # Behat\MinkExtension\Context\MinkContext::fillField()
And I press "Ok" # Behat\MinkExtension\Context\MinkContext::pressButton()
Then I should see "2" in the "#result" element # Behat\MinkExtension\Context\MinkContext::assertElementContainsText()
Element matching css "#result" not found.
1 scenario (1 failed)
4 steps (3 passed, 1 failed)
0m0.048s
Two steps closer to success! The latest step is still failing, so to make it pass we will complete our HTML with hardcoded value (remember the TDD philosophy: find the simplest way to make the tests pass):
<html>
<body>
<form action="/">method="post">
<input type="text">name="operation" />
<button type="submit">Ok</button>
</form>
<div id="result">2</div>
</body>
</html>
Then … you guessed it… run Behat:
~/calculator $ bin/behat
Feature: Calculator calculates operations and returns it to you
Scenario: Calculate 1+1 and return 2 # features/calculator.feature:3
Given I am on "/" # Behat\MinkExtension\Context\MinkContext::visit()
When I fill in "operation" with "1+1" # Behat\MinkExtension\Context\MinkContext::fillField()
And I press "Ok" # Behat\MinkExtension\Context\MinkContext::pressButton()
Then I should see "2" in the "#result" element # Behat\MinkExtension\Context\MinkContext::assertElementContainsText()
1 scenario (1 passed)
4 steps (4 passed)
0m0.041s
Green bar!! Ok this was the fourth step of our TDD process (run the tests and succeed), easy isn’t it?!
To go further we need to think about our calculator. Adding 1 to 1 was simple and we did it quite easily, but what if we want to add 2 to 3? Write the feature:
#feature/calculator.feature
Feature: Calculator calculates operations and returns it to you
# ...
Scenario: Calculate 2+3 and return 5
Given I am on "/"
When I fill in "operation" with "2+3"
And I press "Ok"
Then I should see "5" in the "#result" element
Of course, running Behat again, this scenario will fail as we hardcoded the result in our HTML. So we will need to change it without breaking the first scenario…
Here is the code:
<?php
$result = "";
// The operation was submitted
if (!empty($_POST)) {
$operation = $_POST['operation'];
$result = eval("return $operation;");
}
?>
<html>
<body>
<form action="/" method="post">
<input type="text" name="operation" />
<button type="submit">Ok</button>
</form>
<div id="result">
<?php echo $result ?></div>
</body>
</html>
Running Behat, everything is ok.
Does your calculator do something else than addition?
Yes! I will prove it! Add the following scenarios and run Behat again:
# features/calculator.feature
Feature: Calculator calculates operations and returns it to you
# ...
Scenario: Calculate 10-5 and return 5
Given I am on "/"
When I fill in "operation" with "10-5"
And I press "Ok"
Then I should see "5" in the "#result" element
Scenario: Calculate 2*3 and return 6
Given I am on "/"
When I fill in "operation" with "2*3"
And I press "Ok"
Then I should see "6" in the "#result" element
Scenario: Calculate 4/2 and return 2
Given I am on "/"
When I fill in "operation" with "4/2"
And I press "Ok"
Then I should see "2" in the "#result" element
~/calculator $ bin/behat --format=progress
................................
5 scenarios (5 passed)
20 steps (20 passed)
0m0.155s
So yes, our calculator can add, substract, multiply and divide.
Refactor: step 5 of TDD
Adding scenarios for each type of calculation we duplicate steps, let’s see how to keep our feature small but readable, using Scenario outlines:
Feature: Calculator calculates operations and returns it to you
Scenario Outline: Calculate an operation and print the result
Given I am on "/"
hen I fill in "operation" with "<operation>"
And I press "Ok"
Then I should see "<result>" in the "#result" element
Examples:
| operation | result |
| 1+1 | 2 |
| 2+3 | 5 |
| 10-5 | 5 |
| 2*3 | 6 |
| 4/2 | 2 |
Run Behat:
~/calculator $ bin/behat --format=progress
............................
5 scenarios (5 passed)
20 steps (20 passed)
0m0.135s
Much cleaner, isn’t it? Yes, but we can improve its quality a little bit more. We tested that 1+1 equals 2 and 2+3 equals 5, we can remove one of these examples as it is quite the same.
Up to you!
If you want to go further you may add some functionalities to our Calculator. Improve the web interface, add buttons, separate view from controller…
Finally, if you are interested in BDD you should read this now classic blog post from Dan North’s blog and to learn more about TDD you should read the “Test-Driven Development by example” by Kent Beck.
Hope this article will help you start with Behat on your next PHP project!