Introduction
The purpose of this guide is not to cover what Behaviour-driven development (BDD) is. We assume you already have experience with this methodology.
In case you want to have an understanding of what BDD is, you can refer to Whats In a Story? web page.
For our basic usage on BDD, we will test duckduckgo.com
interface. If you have not yet installed Athena, refer to Athena project page.
Project Setup
We will create a simple directory structure, which we will use as our workspace.
mybdd-tests/
├── Base
│ ├── Context
│ │ └── FeatureContext.php
│ └── Feature
│ └── AnonymousUserSearch.feature
├── Report
├── athena.json
└── behat.yml
athena.json
is where Athena reads the configuration from. In this case it will contain only the necessary information for a simple BDD test run.
behat.yml
our Behat configuration—with some customizations.
Report
directory is where the HTML report, generated by Athena, will be output.
Base\Feature\AnonymousUserSearch.feature
is our story narrative with our different scenarios.
Base\Context\FeatureContext.php
is where words meet code. Our story implementation.
The athena.json
Athena configuration file is pretty straightforward and requires very little configuration. For now we need only a couple of keys.
The selenium.hub_url
The first thing you specify in athena.json
file, is the selenium.hub_url
key. You're telling Athena where the interface for manipulating the browser is located.
{
"selenium" : {
"hub_url" : "http://athena-selenium-hub:4444/wd/hub"
},
As you can see selenium.hub_url
key, is pointing to our—local Selenium set-up.
selenium-hub
docker container is linked with Athena's PHP container, and is where athena-selenium-hub
is mapped.
Please refer to Athena Selenium Plugin documentation.
The report
One of our goals is to debug what actions were performed by the Browser. We don't need too much detail, but enough to understand what happened, specially in case something goes wrong.
"report" : {
"format" : "html",
"outputDirectory" : "./Report"
}
}
Setting up a report is fairly easy, you just have to define the report.format
, and report.outputDirectory
. We will make use of our ./Report
directory to keep things nice and tidy.
The behat.yml
# behat.yml
default:
extensions:
Athena\Behat\BootstrapFileLoader:
bootstrap_path: "/opt/athena/bootstrap.php"
Athena\Event\Proxy\BehatProxyExtension: ~
suites:
default:
paths:
- %paths.base%/Base/Features
contexts:
- Tests\Base\Context\FeatureContext
A first look at Behat's configuration file might be scary, although, as you iterate over the different configurations and understand their purpose, everything becomes simple.
Custom extensions
Athena\Behat\BootstrapFileLoader:
bootstrap_path: "/opt/athena/bootstrap.php"
In order to access Athena Programming Interface we need a way to inject its bootstrap (autoloader, etc). That's exactly BootstrapFileLoader
job.
Athena php
code is mounted inside a docker container, more specifically in /opt/athena
. This explains the strange path string you see.
Athena\Event\Proxy\BehatProxyExtension: ~
A second look at the extension section makes BehatProxyExtension
stand-out. For generating reports, we register a listener for each of Behat's events and then convert them in—beautiful HTML markup.
Unfortunately Behat does not provide you a nice interface for registering event listening, unless you create your own extension. Which we did.
Default Suite
suites:
default:
paths:
- %paths.base%/Base/Features
contexts:
- Tests\Base\Context\FeatureContext
Behat let's you define different configurations for each test suite, although for the purpose of this guide, we have only the need for a single suite, the default.
The reason for a custom directory structure is: Consistency over our different projects. So far we have adopted directories to start with a capital character, so if we keep working like that, our brain doesn't have to be constantly learning new standards, less distractions.
Our feature files are located inside Base/Features
, and that's where our configuration file tells Behat to look for *.feature
files.
When it comes to Context classes, they must be defined in Behat's configuration file as well. For this guide we have a single one, where we will translate our story steps to actual code. I highly recommend you have a good look at good practices and context re-use in Behat's website—after completing this guide.
The namespace used for the Context class will be explained later on.
Writing a Test
The Story Telling
Feature: Anonymous User performs a search
As a Anonymous User
I want to perform a search for a string
So that I can get a list of results related with my search
Scenario: Searched string returns results
Given the current location is the home page
When the Anonymous User writes "athena" in the search box
And the Anonymous User performs a click in the search button
Then the current location should be results page
And the results count should be greater than "0"
The beauty of story telling is how easily we can understand the whole flow. Just by reading you know, or can imagine, exactly the piece of code to be written.
Something interesting is how easily we can also come up with patterns. Most steps can be converted to use parameters and then re-use these steps. It's an interesting exercise to do—after completing this guide.
Our story file is located at Base\Feature\AnonymousUserSearch.feature
.
From Words to Code
<?php
namespace Tests\Base\Context;
use Athena\Test\AthenaTestContext;
/**
* Features context.
*/
class FeatureContext extends AthenaTestContext
{
}
The Namespace
One of the caveats of writing a test in Athena, is the namespace. It should—always—start with—Tests\
.
Internally Athena will map Tests\
to your testing directory.
This behaviour gives you freedom to choose how you organise the directory, where you store your tests.
The Parent Class
Another close look to the code will make our parent class, AthenaTestContext
stand-out, and you'll ask yourself what it does, if you didn't, you have now. When building our tests we should—always—include Athena's test cases.
Each type of test is wrapped with a Athena class of it's own, as you can imagine, this introduces custom behaviour when needed. I won't cover here all the types, as the focus is our BDD test case. After completing the guide, I recommend you giving them a—quick—look.
The Browser Navigation
/**
* @var \Athena\Browser\Page\PageInterface
*/
private $currentLocation;
/**
* @Given /^the current location is home page$/
*/
public function theCurrentLocationIsHomePage()
{
$this->currentLocation = Athena::browser()->get('https://duckduckgo.com');
}
Our first step expects our current location to be the homepage. That's exactly what we do when calling Athena::browser()->get('https://duckduckgo.com')
, our browser will navigate to the given address and return an interface to act upon the page.
Important note is Athena::browser()
returns always the current active Browser instance.
The Interaction With Elements
/**
* @When /^the Anonymous User writes "([^"]*)" in the search box$/
*/
public function theAnonymousUserWritesInTheSearchBox($arg1)
{
$this->currentLocation
->find()
->elementWithName('q')
->sendKeys($arg1);
}
/**
* @Given /^the Anonymous User performs a click in the search button$/
*/
public function theAnonymousUserPerformsAClickInTheSearchButton()
{
$this->currentLocation
->find()
->elementWithId('search_button_homepage')
->click();
}
Next two steps make use of PageFinderInterface
to search for our elements and act on them. We simply want to search and perform basic actions, although you can assert that elements meet a certain criteria.
Out Of The Ordinary Assertions
/**
* @Then /^the current location should be results page$/
*/
public function theCurrentLocationShouldBeResultsPage()
{
PHPUnit_Framework_Assert::assertStringMatchesFormat("https://duckduckgo.com/?q=athena", Athena::browser()->getCurrentURL());
}
/**
* @Given /^the results count should be greater than "([^"]*)"$/
*/
public function theResultsCountShouldBeBiggerThan($arg1)
{
$results = $this->currentLocation
->find()
->elementsWithCss('.result');
PHPUnit_Framework_Assert::assertGreaterThan($arg1, count($results));
}
Some assertions are not yet covered by Athena programming interface, although on out of the ordinary situations you can take advantage of underlaying technologies, if it does—not—compromise code structure or introduce more complexity and confusion.
All Pieces Together
Our FeatureContext.php
file is now finished.
<?php
namespace Tests\Base\Context;
use Athena\Athena;
use Athena\Test\AthenaTestContext;
use PHPUnit_Framework_Assert;
/**
* Features context.
*/
class FeatureContext extends AthenaTestContext
{
/**
* @var \Athena\Browser\Page\PageInterface
*/
private $currentLocation;
/**
* @Given /^the current location is the home page$/
*/
public function theCurrentLocationIsTheHomePage()
{
$this->currentLocation = Athena::browser()->get('https://duckduckgo.com/');
}
/**
* @When /^the Anonymous User writes "([^"]*)" in the search box$/
*/
public function theAnonymousUserWritesInTheSearchBox($arg1)
{
$this->currentLocation
->find()
->elementWithName('q')
->sendKeys($arg1);
}
/**
* @Given /^the Anonymous User performs a click in the search button$/
*/
public function theAnonymousUserPerformsAClickInTheSearchButton()
{
$this->currentLocation
->find()
->elementWithId('search_button_homepage')
->click();
}
/**
* @Then /^the current location should be results page$/
*/
public function theCurrentLocationShouldBeResultsPage()
{
PHPUnit_Framework_Assert::assertStringMatchesFormat("https://duckduckgo.com/?q=athena", Athena::browser()->getCurrentURL());
}
/**
* @Given /^the results count should be greater than "([^"]*)"$/
*/
public function theResultsCountShouldBeBiggerThan($arg1)
{
$results = $this->currentLocation
->find()
->elementsWithCss('.result');
PHPUnit_Framework_Assert::assertGreaterThan($arg1, count($results));
}
}
Execute The Test
Athena runs it's tests through the command line interface, so we'll need to navigate inside Athena's project directory, to access it's executable.
$ athena php bdd
...
usage: athena php bdd <tests-directory> <config-file> [<options>...] [<behat-options>...]
<tests-directory> This directory will be mounted inside the docker container. Behat will be executed inside this directory
<config-file> Athena config file, with proxy configurations, grid options, etc
[--browser=<name>] Browser name to be used. Such as firefox, phantomjs, or chrome
[--parallel-process=<number>] Number of scenarios, of a single feature, to be ran in parallel
[--parallel-features=<number>] Number of features to be ran in parallel. This can be used with --parallel-process to achieve the best results
[--php-version=<version>] Switch between available PHP versions. E.g. --php-version=7.0
[--override-athena-dependencies] Override PHP plugin dependencies with the ones found inside the tests directory
[--restore-athena-dependencies] Restore PHP plugin original dependencies
Writing athena php bdd
and hitting enter, will show you the basic usage, on the requirements to run a bdd test case. Most likely by now you already know the next steps.
$ athena php bdd ../mybdd-tests ../mybdd-tests/athena.json --browser=firefox
Once you run that command, if it is your first time running athena, you'll most likely see a lot of output. This is Athena setting up it's docker images.
When it's all completed, or you are running the command a second time, after having everything installed, you should see the following:
...
Feature: Anonymous User performs a search
As a Anonymous User
I want to perform a search for a string
So that I can get a list of results related with my search
Scenario: Searched string returns results
Given the current location is the home page
When the Anonymous User writes "athena" in the search box
And the Anonymous User performs a click in the search button
Then the current location should be results page
And the results count should be greater than "0"
1 scenario (1 passed)
5 steps (5 passed)
0m6.86s (14.83Mb)
Execute a Single Feature
Sometimes during development time we need to run a single test. There are two ways we can do that, either through tagging (read more about this in Behat's documentation page) the test or take advantage of Athena CLI interface.
$ athena php bdd ../mybdd-tests ../mybdd-tests/athena.json --browser=firefox Base/Feature/AnonymousUserSearch.feature
Behat will construct the path to the feature relative to behat.yml
location. That's why we start at Base\
, since our behat.yml
is located at the same directory level.
Reading The Report
In our configuration file, we've specified Report/
as our output directory for the report file, so that where we will be looking for it.
mybdd-tests/Report
├── athenaimg_56d5bd53cfc79.jpg
├── athenaimg_56d5bd5460f48.jpg
├── athenaimg_56d5bd56ce02e.jpg
├── athenaimg_56d5bd575a8d6.jpg
├── athenaimg_56d5bd57ed91d.jpg
└── report.html
Open report.html
in your browser, and you should see nice HTML report containing all the steps we took, together with screenshots for each one.
Configure Proxy and/or Grid Hub
When athena php bdd
is run, it will try to automatically link with a running Proxy Server (athena proxy start
).
If you specify --browser
, it will try to automatically link with a running Grid Hub (athena selenium start hub
).
In case --skip-proxy
or/and --skip-hub
exists, the link will not be performed.
For performing a link with another running container, you can optionally specify --link-proxy=<container_name>
and/or --link-hub=<container_name>
.
Parallel Tests
$ athena php bdd my-tests/ my-tests/athena.json [--browser=<name>] --parallel-features=<number> --parallel-process=<number>
Features in Parallel
$ athena php bdd example-tests/ example-tests/athena.json --parallel-features=2
Scenarios in Parallel
$ athena php bdd example-tests/ example-tests/athena.json --parallel-process=2
Features and Scenarios in Parallel
In this example, will run two features in parallel, and each feature, will run two scenarios in parallel.
$ athena php bdd example-tests/ example-tests/athena.json --parallel-features=2 --parallel-process=2