In Behat, anything that doesn’t throw an exception is treated as a success. Every custom step definition presents the developer with the responsibility to check for exceptions and the opportunity to increase the value of scenario automation by providing meaningful feedback about failure.
We’ll explore this opportunity in the custom step definitions in the following scenario for a site whose main source of income results from presenting a discounted sale product on the front page:
Scenario: Daily Deal discount Given I am on the homepage When I click the Daily Deal "Buy now!" link Then I should see the product title And the sale price should reflect the discount advertised on the homepage
The product and discount change daily, so instead of matching literal text, the first custom step will use a css selector to find the discount amount and product title.
To start implementing, run the scenario to generate stubs for the custom steps. Note: --append-snippets can be used to write the output directly to the FeatureContext.php file.
It wouldn’t be uncommon to find the first custom step implemented with something like:
/** * @When /^I click the Daily Deal "([^"]*)" link$/ */ public function iClickTheDailyDealLink($linkText) { $page = $this->getSession()->getPage(); // Limit to the Daily deal block $el = $page->find('css','#daily'); // Find the title for use in the next step $this->product = $el->find('css','h2')->getText(); // Find the discount amount for use in the next step $this->discount = $el->find('css','span#dd-discount')->getText(); // Go to the product page $link = $el->findLink($linkText); if (empty($link)) { throw new Exception('Link not found'); } $link->click(); }
There are many ways to improve this step definition, but with respect to exceptions, the first and perhaps most important thing is:
Every Action Needs an Exception
Do not act on something without verifying that it has returned a value. For example, combining find()
with getText()
above will result in “PHP Fatal error: Call to a member function getText()
on a non-object” if the find()
does not succeed. That can be addressed easily enough during active development, but when the test is running as part of continuous integration or under pressure before a big deployment, a fatal error will quite frustratingly kick you out of the whole test run.
To prevent fatal errors and provide precise feedback, supply exceptions:
/** * @When /^I click the Daily Deal "([^"]*)" link$/ */ public function iClickTheDailyDealLink($linkText) { $page = $this->getSession()->getPage(); $el = $page->find('css','#daily'); if(empty($el)) { throw new Exception("Element not found."); } // find the title for use in a later step $product = $el->find('css','h2'); if(empty($product)) { throw new Exception("Element not found."); } $this->product = $product->getText(); // find the discount amount for use in a later step $discount = $el->find('css','span#dd-discount'); if(empty($discount)) { throw new Exception("Element not found."); } if (empty($link)) { throw new Exception('Link not found'); } $link->click(); }
More Detailed Feedback
“Link not found” can be plenty of information when you’re actively working on the code that provides the link, and while the project is fresh in your mind, but later it leaves a lot of detective work. To diagnose a failure in the step above, you need to navigate to the application and inspect the elements to see what’s actually happening. Navigating the click path is simple in this example, but it can be quite complicated and time consuming, and it can also require a noticeable cognitive shift to get back into the project head space.
Even more important, later in the life cycle of the project, it’s very possible that the first person to try to interpret a test failure won’t be a developer at all, and may well be someone who never knew the project context. For all these reasons, cultivating the habit of providing context will contribute greatly to the long-term value of automation.
Include What You Were Looking For
Custom step definitions which do nothing more than narrow down a web page to a specific section and then verify or act on an element with it are so common, it’s probably worth abstracting the message with a helper function and improving the feedback:
public function selectorNotFound($selector) { return sprintf("The selector '%s 'was not found.", $selector); } … // narrow to the Daily Deal block $selector = '#daily'; $el = $page->find('css',$selector); if (empty($el)) { throw new Exception ($this->selectorNotFound($selector)); }
Output:
The selector "#daily" was not found.
Print the URL
One very straightforward habit is to provide the URL of the page where the failure occurred. This is so frequently useful, it’s worth creating a helper function for this, too:
public function url() { return sprintf("Error on '%s'. \n", $this->getSession()->getCurrentUrl()); }
Here’s an implementation of the next step, combining these ideas:
/** * @Then /^I should see the product title$/ */ public function iShouldSeeTheProductTitle() { $page = $this->getSession()->getPage(); $selector = 'h1'; $title = $page->find('css',$selector); if(empty($title)) { throw new Exception ($this->url() . $this->selectorNotFound($selector)); } $title = $title->getText(); if($title != $this->product) { throw new Exception ($this->url()); } }
If there’s a change in markup, you’ll see:
Error on http://example.com/turnip-twaddler. The selector "h1" was not found.
If there’s no matching product title, you’ll see:
Error on http://seven.l/turnip-twaddler. ‘Turnip Twaddler’ does not match ‘Karrot Kutter’
Creating specific and very readable exceptions opens the possibility of keeping developers focused on coding – and offloading the reproduction and triage of failures to people on your team who spend more time with the application functionality in the first place. It requires some initial adjustments, but once the habits are in place, those habits pay off in the long term.