over 5 years ago

## 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:

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. Second step we can define with the following regexp: /^I(?: should)? see "([^"]*)" in the 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:

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:

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

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:

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:

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.