вторник, 28 августа 2012 г.

Context-dependent Behat tests steps

Preamble is this: we have the PHP-based website, and we are testing it with the Behat+Mink+MinkExtension combo.

Suppose we want to write the following test scenario:

When I am in the Friends section

… (something there) …

Then I should see “My Friend” in search results

Let's define this steps in our FeatureContext. First step we can define with the following regexp: /^I am in the Friends section$/ because we really don’t need the method of FeatureContext class containing long switch enumerating every possible section of the site.


/**
 * @Given /^I am in the "Friends" section$/
 */
public function iAmInTheFriendsSection() {
 return new Given('I am on "/friends"');
}

Second step we can define with the following regexp: /^I(?: should)? see "([^"]*)" in the search results$/.


/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {

 // separate helper function to search the "search results" HTML element
 $search_results = $this->getSearchResultsElement();

 // separate helper function to search the $search_term text in $search_results element
 $this->trySearchTextOnDomElement($search_term, $search_results);

}

It should be obvious why we use the custom test step instead of using the predefined test steps and writing something like 'I should see "My Friend" in ".search-wrapper form input[role="search"]" element'.

Then, someday, sure thing, we will want to write the following scenario:

When I am in the Shop section

… (something there) …

Then I should see “Interesting product” in search results

And in here, we have another “search results”, which should be found by completely different selector and which is located on different page.

So, this is the context-dependent statement: term “search results” depends on what “section” we mentioned previously. This is right from the linguistics. To be able to use this natural-language feature we need to implement it somehow.

I'll use the abbrev CDTS instead of longer "context-dependent test step".

Fortunately, Behat has a feature with exactly the same purpose: subcontexts. Unfortunately, it's not working in the way we need to use the CDTS properly.

In an ideal world, we can do this:


/**
* @Given /^I am in the Friends section$/
*/
public function iAmInTheFriendsSection() {
 $this->useContext('friends_section', new FriendsSectionContext())
 return new Given('I am on "/friends"');
}

and this would load the FriendsSectionContext and all CDTS definitions in it, like the following:


// in FriendsSectionContext class
/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {
 // This selector is valid only in the context of "Friends" section.
 $search_results_selector = '#PeopleFinder #pf_all .results';

 // Logic to check if the given $search_term is the present in anything called "search results" in the context of "Friends" section.
 $search_results = $this->getSession()->getPage()->find('css', $search_results_selector);
 $search_term_present = strpos($search_results->getHtml(), $search_term);
 if ($search_term_present === false) {
  throw new Exception(...);
 }
}

We useContext different context class, we get different definition for the /^I should see "([^"]*)" in the search results$/ test step.

Unfortunately, Behat cannot load the test step definitions from subcontexts at runtime. Apparently, it’s because it should parse the regexps in docblocks corresponding to definitions or something like that. So, are forced to load all our subcontexts right in our constructor.

Apart from being horribly ineffective, this prevents us from defining the test steps having same regexp across several different separate subcontexts.

Workaround for this problem is this:

  1. add the property to the FeatureContext which will hold the reference to current subcontext, name it like "location_context" or so,
  2. make the context-setting ('I am in the "..." section') test step set the "location_context" to the subcontext needed (you can get the subcontext with the call to getSubcontext('alias')),
  3. move the context-dependent logic to “normal” subcontext methods, which should have the same name across all subcontexts,
  4. register all subcontexts with useContext under meaningful aliases like "friends_section", "shop_section", etc,
  5. define the context-dependent test step like 'I should see "..." in search results' in main FeatureContext class,
  6. in the definition of this step, get the context-dependent logic needed by calling the relevant method on the subcontext the "location_context" property currently points at.

So, we need our context-setting test steps to be like this:


/**
 * @Given /^I am in the "Friends" section$/
 */
public function iAmInTheFriendsSection() {
 $this->location_context = $this->getSubcontext('friends_section');
 return new Given('I am on "/people"');
}

Assuming 'friends_section' is an alias of the FriendsSectionContext, and it was set in the constructor, after this test step, our "location_context" will be FriendsSectionContext, and, say, it's getSearchResultsElement() will do exactly what we need in the "Friends" section.

Then, the context-depentent test step will be like this, getting the location-dependent logic from the "location_context" set previously:


/**
 * @Then /^I should see "([^"]*)" in the search results$/
 */
public function iShouldSeeInTheSearchResults($search_term) {
 // getSearchResultsElement() is defined in subcontext which was set before in $this->location_context
 $search_results = $this->location_context->getSearchResultsElement();
 $search_term_present = strpos($search_results->getHtml(), $search_term);
 if ($search_term_present === false) {
  throw new Exception(...);
 }
}

Main point is this: we want to check if something should appear in the "search results" entity in some different page → we can use the same test step in our .feature files, just explicitly name the section needed beforehand somewhere above in the text. This will make the .feature files a lot more human-readable.

This concludes the explanation about how to use this linguistic technique in Behat tests.