An in-depth look at intermediate to advanced level Behat topics. We'll cover writing better features, regular expressions in steps, Context files, changes from Behat 2 to 3, and more,
7. Writing Better .Feature Files
Scenario: Visit Seminar Page before Broadcast Time
Given I want to watch the video called "Future Seminar"
When I visit that seminar's page
Then I should see "Future Seminar" on the page
And I should see "Future Seminar Author" on the page
And I should see "This seminar begins at 6:00 pm EST" on the page
And I should see a countdown timer
8. “What is behavior-driven development,
you ask? It’s the idea that you start by
writing human-readable sentences that
describe a feature of your application
and how it should work, and only then
implement this behavior in software.”
- Behat Documentation
9. Writing Better .Feature Files
Scenario: Visit Seminar Page before Broadcast Time
Given there is a seminar scheduled for the future
When I visit that seminar's page
Then I should see the seminar's name
And I should see the seminar's author’s name
And I should see "This seminar begins at"
And I should see the seminar’s start time in EST
And I should see a countdown timer
10. Why?
● Easier for Programmers to Understand
● Helps Prevent Regression
● Easier for Business Users to Understand
● Easier for Business Users to Write
● Can Identify Bad Code!
11. How Better .Feature Files Can Identify Bad Code
Original:
Scenario: Display Local Services in Product Catalog
Given I view the catalog
When I select "Texas" from the states list
Then I should see the list of services offered
Rewritten:
Scenario: Display Local Services in Product Catalog
Given I view the catalog
When I select a state from the states list
Then I should see the list of services offered
12. How Better .Feature Files Can Identify Bad Code
Original:
Scenario: Display Local Services in Product Catalog
Given I view the catalog
When I select "Texas" from the states list
Then I should see the list of services offered
Rewritten:
Scenario: Display Local Services in Product Catalog
Given we have a regional office offering local services in a state
When I view the catalog
And I select that state from the states list
Then I should see the list of services offered for that state
13. Feature Description - As A, In Order To, I Need...
Feature: Display Texas-Specific Local Services
As a company, we offer specific services only in Texas
In order to sell these services to the right people
We need to display the services when users are browsing our catalog
for Texas
Scenario: Display Local Services When Texas is Selected
Given I view the catalog
When I select Texas from the states list
Then I should see the list of services offered
14. When I select Texas from the states list
/**
* @When /^I select Texas from the states list$/
*/
public function iSelectTexasFromTheStatesList(){}
When I select "Texas" from the states list
/**
* @When /^I select "([^"]*)" from the states list$/
*/
public function iSelectFromTheStatesList($arg1){}
A Clear Behavior
15. Negative Cases
Scenario: Display Product Catalog
Given I view the catalog
When I select a state from the list
Then I should see the list of products for sale in that state
Scenario: Display Local Services
Given we have a regional office that offers local services in a
state
When I view the catalog
And I select that state from the states list
Then I should see the list of services offered for that state
17. Negative Cases
Scenario: Display Product Catalog
Given I view the catalog
When I select a state from the list
Then I should see the list of products for sale in that state
Scenario: Display Local Services
Given we have a regional office that offers local services in a state
When I view the catalog
And I select that state from the states list
Then I should see the list of services offered for that state
Scenario: Don’t Display Local Services When No Regional Office
Given a state has no regional office offering local services
When I view the catalog
And I select that state from the states list
Then I should not see a list of services
20. Auto-Generated Steps
You can implement step definitions for undefined steps with these snippets:
/**
* @When /^I select Texas from the states list$/
*/
public function iSelectTexasFromTheStatesList()
{
throw new PendingException();
}
/**
* @Then /^I should see the list of services offered$/
*/
public function iShouldSeeTheListOfServicesOffered()
{
throw new PendingException();
}
31. Fixture Data
namespace AcmeAppBundleDataFixtures;
use DoctrineCommonPersistenceObjectManager;
use DoctrineCommonDataFixturesFixtureInterface;
class UserFixtureLoader implements FixtureInterface
{
public function load(ObjectManager $manager)
{
$user = new User();
$user->setUsername('admin');
$user->setPassword('password');
$manager->persist($user);
$manager->flush();
}
}
32. Load Fixture Data Hook
/** @BeforeFeature */
public function beforeFeatureReloadDatabase($event)
{
$loader = new Loader();
$directory = __DIR__ . DIRECTORY_SEPARATOR .
'..' . DIRECTORY_SEPARATOR . 'DataFixtures';
$loader->loadFromDirectory($directory);
$entityManager = $this->getEntityManager();
$purger = new ORMPurger();
$executor = new ORMExecutor($entityManager, $purger);
$executor->execute($loader->getFixtures());
}
34. Multiple Regular Expressions
/**
* @Given /^I view the catalog$/
* @Given /^I am viewing the catalog$/
*/
public function iViewTheCatalog(){
$this->getPage('Catalog')->open();
}
35. Case Insensitive - Flag
When I view the catalog
When I view the Catalog
/**
* @Given /^I view the catalog$/i
*/
public function iViewTheCatalog(){
$this->getPage('Catalog')->open();
}
36. Case Insensitive - Inline
When I view the catalog
When I view the Catalog
/**
* @Given /^I view the (?i)catalog$/
*/
public function iViewTheCatalog(){
$this->getPage('Catalog')->open();
}
37. Unquoted Variables
Then I should see an "error" message
/**
* @Given /^I should see an "([^"])" message$/
*/
public function iShouldSeeAnMessage($arg1){
}
38. Unquoted Variables
Then I should see an error message
/**
* @Given /^I should see an (.*) message$/
*/
public function iShouldSeeAnMessage($arg1){
}
39. Unquoted Variables with List of Options
Then I should see an error message
Then I should see a success message
Then I should see a warning message
/**
* @Given /^I should see an? (error|success|warning) message$/
*/
public function iShouldSeeAnMessage($messageType){
$class = '.alert-'.$messageType;
$this->assertElementExists($class, 'css');
}
40. Optional Variables
Then I should see an error message
Then I should see an error message that says " Stop!"
/**
* @Given /^I should see an? (error|success|warning) message$/
* @Given /^I should see an? (error|success|warning) message that says
"([^"])"$/
*/
public function iShouldSeeAnMessageThatSays ($messageType, $message = null)
{
$class = '.alert -' . $messageType;
$this->assertElementExists ($class, 'css');
if ($message !== null) {
$this->assertElementContainsText ($class, 'css', $message);
}
}
41. Non-Capturing Groups
Then I view the catalog for "Texas"
Then I am viewing the catalog for "Texas"
/**
* @Given /^I view the catalog for "([^"]*)"$/
* @Given /^I am viewing the catalog "([^"]*)"$/
*/
public function iViewTheCatalogForState($stateName)
{
$args = ['stateName' => $stateName];
$this->getPage('Catalog')->open($args);
}
42. Non-Capturing Groups
Given I am viewing the catalog for “Texas”
Given I viewing the catalog for “Texas”
<?php
/**
* @Given /^I (?:am viewing|view) the catalog for "([^"]*)"$/
*/
public function iViewTheCatalogForState($stateName){
$this->getPage(‘Catalog’)->open([‘stateName’=>$stateName);
}
Then I view the catalog for "Texas"
Then I am viewing the catalog for "Texas"
/**
* @Given /^I (?:am viewing|view) the catalog for "([^"]*)"$/
*/
public function iViewTheCatalogForState($stateName)
{
$args = ['stateName' => $stateName];
$this->getPage('Catalog')->open($args);
}
43. Step Definition Changes in Behat 3.x
Given I am viewing the catalog for “Texas”
Given I viewing the catalog for “Texas”
<?php
/**
* @Given /^I (?:am viewing|view) the catalog for "([^"]*)"$/
*/
public function iViewTheCatalogForState($stateName){
$this->getPage(‘Catalog’)->open([‘stateName’=>$stateName);
}
Then I view the catalog for "Texas"
Then I view the catalog for Texas
/**
* @Given I view the catalog for :stateName
*/
public function iViewTheCatalogForState($stateName)
{
$args = ['stateName' => $stateName];
$this->getPage('Catalog')->open($args);
}
45. SubContext (Behat 2.x)
<?php
namespace AcmeAppBundleContext;
use BehatMinkExtensionContextMinkContext;
class FeatureContext extends MinkContext
{
public function __construct(array $parameters)
{
$this->parameters = $parameters;
$this->useContext('MessageContext', new MessageContext());
}
}
46. Several SubContexts (Behat 2.x)
[...]
public function __construct(array $parameters)
{
$this->parameters = $parameters;
$this->useContext('AdminContext', new AdminContext());
$this->useContext('FormContext', new FormContext());
$this->useContext('EditUserContext', new EditUserContext());
$this->useContext('ApiContext', new ApiContext());
}
47. Alias All SubContexts Automatically (Behat 2.x)
private function loadSubContexts ()
{
$finder = new Finder();
$finder->name('*Context.php' )
->notName('FeatureContext.php' )
->notName('CoreContext.php' );
$finder->files()->in(__DIR__);
48. Alias All SubContexts Automatically (Behat 2.x)
private function loadSubContexts ()
{
$finder = new Finder();
$finder->name('*Context.php' )
->notName('FeatureContext.php' )
->notName('CoreContext.php' );
$finder->files()->in(__DIR__);
foreach ($finder as $file) {
$className = $file->getBaseName('.php');
$namespace = __NAMESPACE__ . '' . $file->getRelativePath ();
if (substr($namespace, -1) !== '') {
$namespace .= '';
}
$reflectionClass = new ReflectionClass ($namespace . $className);
$this->useContext($className, $reflectionClass ->newInstance());
}
}
49. <?php
namespace AcmeAppBundleContext;
class FeatureContext extends CoreContext
{
/** @Given /^I should see an? (error|success|warning) message that says
"([^"])"$/ */
public function iShouldSeeAnMessageThatSays ($messageType, $message =
null)
{
$class = '.alert -' . $messageType;
$element = $this->getPage()->find('css', $class);
$actualMessage = $element->getText();
$this->assertEqual($actualMessage , $message);
}
}
Message Context
50. Find Required Element Shortcut
public function findRequiredElement ($locator,
$selector = 'xpath',
$parent = null)
{
if (null === $parent) {
$parent = $this->getPage();
}
$element = $parent->find($selector, $locator);
if (null === $element) {
throw new ElementNotFoundException ($this->getSession(), null,
$selector, $locator);
}
return $element;
}
51. Message Context
<?php
namespace AcmeAppBundleContext;
class FeatureContext extends CoreContext
{
/** @Given /^I should see an? (error|success|warning) message that says
"([^"])"$/ */
public function iShouldSeeAnMessageThatSays ($messageType, $message =
null)
{
$class = '.alert -' . $messageType;
$element = $this->findRequiredElement ($class, 'css');
$actualMessage = $element->getText();
$this->assertEqual($actualMessage , $message);
}
}
52. CoreContext with Step Annotation causes Error
[BehatBehatExceptionRedundantException]
Step "/^I should be redirected to "([^"]*)"$/" is already defined in
AcmeAppBundleContextFeatureContext::iShouldBeRedirectedTo()
AcmeAppBundleContextFeatureContext::iShouldBeRedirectedTo()
AcmeAppBundleContextMessageContext::iShouldBeRedirectedTo()
54. Reusing Multiple Steps
Then I should see an error message
Then I should see a success message
Then I should see a warning message
<?php
/**
* @Given /^I should see an? (error|success|warning) message$/
*/
public function iShouldSeeAnMessage($messageType){
$class = ‘.alert-’.$messageType;
$this->assertElementExists($class, ‘css’);
}
Scenario: Upload a csv file
Given I am viewing the csv import form
When I attach a csv to "Import File"
And I submit the form
Then I should see a success message
And I should see the file review screen
Scenario: Review and Confirm the csv file
Given I have uploaded a csv
And I am viewing the file review screen
When I select a property for each column
And I submit the form
Then I should see a success message
55. Meta-Steps
use BehatBehatContextStep;
class FeatureContext
{
/**
* @Given /^I have uploaded a csv$/
*/
public function iHaveUploadedACsv()
{
return [
new StepGiven('I am viewing the csv import form'),
new StepWhen('I attach a csv to "Import File"'),
new StepWhen('I submit the form')
];
}
56. Meta-Steps With Multi-line Arguments
use BehatBehatContextStep;
use BehatGherkinNodePyStringNode;
class FeatureContext
{
/**
* @Given /^I should see the file review screen$/
*/
public function iShouldSeeTheFileReviewScreen()
{
$content = 'Please review your file .' . PHP_EOL .
'Press Submit to continue';
$pyString = new PyStringNode($content);
return new StepGiven('I should see', $pyString);
}
57. Direct Method Call - Same Context
/**
* @Given /^I should see an error about the file type$/
*/
public function iShouldSeeAnErrorAboutTheFileType ()
{
$message = 'This file type is invalid' ;
$this->iShouldSeeAnMessageThatSays ('error', $message);
}
/**
* @Given /^I should see an? (.*) message that says "([^"])"$/
*/
public function iShouldSeeAnMessageThatSays ($messageType, $message = null)
{
$class = '.alert -' . $messageType;
$this->assertElementExists ($class, 'css');
if ($message !== null) {
$this->assertElementContainsText ($class, 'css', $message);
}
}
58. Direct Method Call to Another Context (Behat 2.x)
/**
* @Given /^I should see an error about the file type$/
*/
public function iShouldSeeAnErrorAboutTheFileType()
{
$message = "This file type is invalid";
$this->getMainContext()
->getSubContext('MessageContext')
->iShouldSeeAnMessageThatSays('error', $message);
}
59. Direct Method Call to Another Context (Behat 2.x)
/**
* @return MessageContext
*/
public function getMessageContext ()
{
return $this->getMainContext ()->getSubContext('messageContext' );
}
/**
* @Given /^I should see an error about the file type$/
*/
public function iShouldSeeAnErrorAboutTheFileType ()
{
$message = "This file type is invalid" ;
$this->getMessageContext ()->iShouldSeeAnMessageThatSays ('error',
$message);
}
61. Store Other Contexts (Behat 3.x)
use BehatBehatContextContext;
use BehatBehatHookScopeBeforeScenarioScope ;
class FeatureContext implements Context
{
/** @var MessageContext */
private $messageContext ;
/** @BeforeScenario */
public function gatherContexts (BeforeScenarioScope $scope)
{
$environment = $scope->getEnvironment ();
$this->messageContext = $environment->getContext('MessageContext' );
}
}
// http://docs.behat.org/en/v3.0/cookbooks/context_communication.html
62. Direct Method Call to Another Context (Behat 3.x)
use BehatBehatContextContext ;
use BehatBehatHookScopeBeforeScenarioScope ;
class FeatureContext implements Context
{
/** @var BehatMinkExtensionContextMinkContext */
private $minkContext;
/** @BeforeScenario */
public function gatherContexts (BeforeScenarioScope $scope)
{
$environment = $scope->getEnvironment ();
$this ->minkContext = $environment ->getContext('BehatMinkExtensionContextMinkContext' );
}
}
// http://docs.behat.org/en/v3.0/cookbooks/context_communication.html
/**
* @Given /^I should see an error about the file type$/
*/
public function iShouldSeeAnErrorAboutTheFileType ()
{
$message = "This file type is invalid" ;
$this->messageContext ->iShouldSeeAnMessageThatSays ('error', $message);
}
63. Reusing Steps
Meta-Steps
● Return a Step or array of
Steps
● Hooks will fire
(could be slow)
● Moving Step definitions
does not break
● Removed in 3.0
Calling Methods
● Like any other method
call
● Hooks do not fire
(typically faster)
● Moving Step definitions
might require refactor
66. Seminar Page Object
Scenario: Visit Seminar Page before Broadcast Time
Given there is a seminar scheduled for the future
When I visit that seminar's page
Then I should see the seminar's name
And I should see the seminar's author’s name
And I should see " This seminar begins at "
And I should see the seminar’s start time in EST
And I should see a countdown timer
Scenario: Visit Seminar Page before Broadcast Time
Given there is a seminar scheduled
When I visit that seminar's page during the broadcast time
Then I should see the seminar's name
And I should see the seminar video
And I should not see a countdown timer
67. Seminar Page Object
<?php
namespace AcmeAppBundlePageObjects;
use SensioLabsBehatPageObjectExtensionPageObjectPage;
class Seminar extends Page
{
protected $path = '/seminar/{id}';
}
71. CSV Report
@javascript
Scenario: View Summary Report
Given a user in a group has registered for a seminar with a company
And I am logged in as an admin
And I am viewing the reports area
When I download the "Summary" report
Then I should see the following columns:
| column |
| Group |
| Company |
| Total |
And I should see that user in the report
72. File Download Test
/**
* @When /^I download the "([^"]*)" report$/
*/
public function iDownloadTheReport ($reportName)
{
$xpath = "//a[normalize-space()=' {$reportName}']";
$link = $this->findRequiredElement ($xpath);
$this->getSession()->visit('view-source:' . $link->getAttribute('href'));
$content = $this->getSession()->getPage()->getContent();
$lines = explode(PHP_EOL, $content);
$this->csvRows = [];
foreach ($lines as $line) {
if (strlen(trim($line))) {
$this->csvRows[] = str_getcsv($line);
}
}
}
73. Zip File Download
@javascript
Scenario: View Large Report
Given I am viewing the reports area
When I click "Export" for the "Extremely Large Report" report
Then a zip file should be downloaded
When I unzip the file and I open the extracted csv file
Then I should see the following columns:
| column |
| Group |
| Company |
| Total |
74. File Download Test
/**
* @When /^I click "Export" for the "([^"]*)" report$/
*/
public function iExportTheReport($reportName)
{
$this->file = $this->getArtifactsDir() . 'download-' . time() . '.zip';
file_put_contents($this->file, $this->getSession()->getDriver() > getContent());
}
/**
* @Then /^a zip file should be downloaded$/
*/
public function aZipFileShouldBeDownloaded()
{
$header = $this->getSession()->getDriver()->getResponseHeaders();
$this->assertContains($header['Content-Type'][0], 'application/forced-download');
$this->assertContains($header['Content-Disposition'][0], "zip");
}
75. File Download Test
/**
* @When /^I unzip the file and I open the extracted csv file$/
*/
public function iUnzipTheFileAndOpenCsvFile ()
{
$zip = new ZipArchive;
$unzipped = $zip->open($this->file);
$csv = $zip->getNameIndex(1);
$zip->extractTo($this->getArtifactsDir() );
$zip->close();
$fileRef = fopen($this->getArtifactsDir().$csv , 'r');
$this->csvContents = [];
while (($data = fgetcsv($fileRef)) !== false) {
$this->csvContents[] = $data;
}
fclose($fileRef);
}
76. Confirm Text In PDF
/**
* @Given /^I should see a PDF with the order total$/
*/
public function iShouldSeeAPdfWithTheOrderTotal()
{
$total = 'Order Total: ' . $this->orderTotal;
$this->getMainContext()->assertPageContainsText($total);
}
@javascript
Scenario: View PDF Receipt
Given I am viewing my order history
When I click "View Receipt" for an order
Then I should see a PDF with the order total
78. Testing a Command Line Process with Behat
Scenario: Test User Import With a Large Data Set
Given the system already has 100000 users
And there is a company with 10000 of the users assigned to it
And an admin has uploaded a spreadsheet for the company with 10000 rows
When the system has begun to process the spreadsheet
And I have waited 1 minute
Then the batch process status should be set to "Running" or "Completed"
And I should see at least 100 new users in the company
79. The System Already Has 100000 Users
/** @Given /^the system already has (d+) users$/ */
public function theSystemAlreadyHasUsers ($numUsers)
{
$userSQL = "INSERT INTO `user`(firstname, lastname, username) VALUES" ;
$userValues = [];
$faker = $this->getFaker();
for ($i = 0; $i < $numUsers; $i++) {
$firstname = addslashes($faker->firstName);
$lastname = addslashes($faker->lastName);
$username = $faker->username . $i; //unique
$userValues[] = "('{$firstname}', '{$lastname}', '{$username}')";
}
$userQuery = $userSQL . implode(', ', $userValues);
$this->getEntityManager ()->getConnection()->exec($userQuery);
}
80. There is a company with 10000 users assigned to it
/** @Given /^there is a company with (d+) of the users assigned to it$/ */
public function thereIsACompanyWithOfTheUsersAssignedToIt ($num)
{
$company = $this->generateCompany ();
$conn = $this->getEntityManager ()->getConnection();
$userCompanySQL = "INSERT INTO `user_company`(user_id, company_id)
SELECT `user`.id, {$company->getId()} FROM `user` LIMIT {$num}";
$conn->exec($userCompanySQL );
$this->getEntityManager ()->refresh($company);
$companyUsersCount = $company ->getUserCompanies ()->count();
$this->assertGreaterThanOrEqual ($num, $companyUsersCount );
$this->company = $company;
$this->companyNumUsers = $companyUsersCount ;
}
81. An Admin Has Uploaded a Spreadsheet
/** @Given /^an admin has uploaded a spreadsheet for the company with (d*) rows$/ */
public function adminHasUploadedSpreadsheetForTheCompanyWithRows($numRows)
{
$faker = $this->getFaker();
$this->filePath = $this->getUploadsDirectory() . 'import -' . $numRows . '.csv';
$fh = fopen($this->filePath, "w");
$rows = 'firstname, lastname, username' . PHP_EOL;
for ($i = 0; $i < $numRows; $i++) {
$firstname = addslashes($faker->firstName);
$lastname = addslashes($faker->lastName);
$username = $faker->username . $i; //add $i to force unique
$rows .= "{$firstname}, {$lastname}, {$username}" . PHP_EOL;
}
fwrite($fh, $rows);
fclose($fh);
$repository = $this->getRepository('BatchProcess');
$this->batchProcess = $repository->create()->setFilename($this->filePath);
$repository->save($this->batchProcess);
}
82. The System Has Begun To Process The Spreadsheet
/**
* @When /^the system has begun to process the spreadsheet$/i
*/
public function theSystemHasBegunToProcessTheSpreadsheet()
{
$command = 'php app' . DIRECTORY_SEPARATOR;
$command .= 'console batch:process --batch_id=';
$command .= $this->batchProcess->getId();
if (substr(php_uname(), 0, 7) == "Windows") {
return pclose(popen("start /B " . $command, "r"));
}
return exec($command . " > /dev/null &");
}
83. I Have Waited 1 Minute
/**
* @When /^I have waited (d+) minutes?$/
*/
public function iHaveWaitedSomeMinutes($num)
{
$seconds = 60;
$outputEvery = 30;
$cycles = ($num * $seconds) / $outputEvery;
for ($i = 0; $i < $cycles; $i++) {
sleep($outputEvery);
echo '.';
}
echo PHP_EOL;
}
84. The Batch Process Status Should Be
/**
* @Given /^the batch process status should be set to "(.*)" or "(.
*)"$/
*/
public function theBatchProcessStatusShouldBeSetTo($statusA,
$statusB)
{
$this->getEntityManager()->refresh($this->batchProcess);
$statusName = $this->batchProcess->getStatus()->getName();
if ($statusName !== $statusA && $statusName !== $statusB) {
throw new Exception("Status is currently: {$statusName}");
}
}
85. I should see at least 100 new users
/**
* @Then /^I should see at least (d+) new users in the company$/
*/
public function iShouldSeeAtLeastNewUsersInTheCompany($num)
{
$company = $this->company;
$this->getEntityManager()->refresh($company);
$companyNumUsersNow = $company->getUserCompanies()->count();
$originalNumUsers = $this->companyNumUsers;
$difference = ($companyNumUsersNow - $originalNumUsers);
$this->assertGreaterThanOrEqual($num, $difference);
}