Buy

Context Organization and Behat Suites

When Behat loads, it reads step definitions from FeatureContext and MinkContext because of the behat.yml setup:

12 lines behat.yml
default:
suites:
default:
contexts:
- FeatureContext
- Behat\MinkExtension\Context\MinkContext
... lines 7 - 12

This is a really powerful idea: instead of having one giant context class, we can break things down into as many small, organized pieces. We might have one context for dealing with adding users to the database and another for the API. If you look at our FeatureContext, we already have two very different ideas mixed together: some functions interact with the terminal and others help deal with a web page.

This is begging to be split into 2 classes. Let's copy FeatureContext and create a new file called CommandLineProcessContext. Update the class name and get rid of anything in here that doesn't help do things with the command line:

72 lines features/bootstrap/CommandLineProcessContext.php
... lines 1 - 9
class CommandLineProcessContext implements Context, SnippetAcceptingContext
{
private $output;
... lines 13 - 16
public function iHaveAFileNamed($filename)
{
touch($filename);
}
... lines 21 - 24
public function iRun($command)
{
$this->output = shell_exec($command);
}
... lines 29 - 32
public function iShouldSeeInTheOutput($string)
{
assertContains(
$string,
$this->output,
sprintf('Did not see "%s" in output "%s"', $string, $this->output)
);
}
... lines 41 - 44
public function moveIntoTestDir()
{
if (!is_dir('test')) {
mkdir('test');
}
chdir('test');
}
... lines 52 - 55
public function moveOutOfTestDir()
{
chdir('..');
if (is_dir('test')) {
system('rm -r '.realpath('test'));
}
}
... lines 63 - 66
public function iHaveADirNamed($dir)
{
mkdir($dir);
}
}

In FeatureContext, do the opposite: remove all the things that have nothing to do with working on a web site. Delete these functions and our before and after scenario hooks:

57 lines features/bootstrap/FeatureContext.php
... lines 1 - 13
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext
{
... lines 16 - 22
public function __construct()
{
}
... lines 26 - 29
public function iFillInTheSearchBoxWith($term)
{
$searchBox = $this->assertSession()
->elementExists('css', 'input[name="searchTerm"]');
$searchBox->setValue($term);
}
... lines 37 - 40
public function iPressTheSearchButton()
{
$button = $this->assertSession()
->elementExists('css', '#search_submit');
$button->press();
}
... lines 48 - 51
private function getPage()
{
return $this->getSession()->getPage();
}
}

That's a lot clearer.

Of course to keep our tests passing, we need to tell Behat about our new context:

13 lines behat.yml
... lines 1 - 3
contexts:
- FeatureContext
- CommandLineProcessContext
- Behat\MinkExtension\Context\MinkContext
... lines 8 - 13

If we run behat now:

$ ./vendor/bin/behat

It should run all of our features: the ls and web stuff. It does, and it works! Ignore the undefined functions - those are from product_admin.feature: we haven't finished that yet.

Multiple Suites

But we can go further. in behat.yml, check out the suites key. Currently, we have one "suite" called default:

13 lines behat.yml
default:
suites:
default:
... lines 4 - 13

But you could have many. What's a suite? It's a combination of a set of feature files and the contexts that should be used for them. Think about it: the ls.feature is the only feature that needs CommandLineProcessContext. And every other feature only needs FeatureContext and MinkContext. This is the perfect use-case for a second suite that I'm going to call commands. In this case, only add the CommandLineProcessContext:

18 lines behat.yml
default:
suites:
default:
... lines 4 - 7
commands:
contexts:
- CommandLineProcessContext
... lines 11 - 18

Remove that from the default suite:

18 lines behat.yml
default:
suites:
default:
contexts:
- FeatureContext
- Behat\MinkExtension\Context\MinkContext
... lines 7 - 18

When you execute Behat, it uses the default suite unless you tell it which one to use with the --suite option. Try it with --suite=commands and then run ls.feature:

$ ./vendor/bin/behat --suite=commands features/ls.feature

Or you can use the -dl option to see only the definition lists associated with the contexts in that suite:

$ ./vendor/bin/behat --suite=commands features/ls.feature -dl

Without --suite, we see definitions for the default suite:

$ ./vendor/bin/behat -dl

And yes, we can go even further by telling each suites which features belong to them. Under the features/ directory, create two new directories called commands and web Let's organize: move ls.feature into commands/ and the other four features into web/. Now, add a paths key to the default suite and set it to [%paths.base%/features/web]:

18 lines behat.yml
default:
suites:
default:
contexts:
... lines 5 - 6
paths: [ %paths.base%/features/web ]
... lines 8 - 18

%paths.base% is a shortcut to the root of the project. For the commands suite, do the same thing to point to commands/:

18 lines behat.yml
default:
suites:
default:
... lines 4 - 7
commands:
... lines 9 - 10
paths: [ %paths.base%/features/commands ]
... lines 12 - 18

Now, if you run the default suite:

$ ./vendor/bin/behat

Behat knows to only execute the features in the web/ directory. With --suite=commands, it only runs the features inside of commands/:

$ ./vendor/bin/behat --suite=commands

So if you have two very different things that are being tested, consider separating them into different suites entirely. But at the very least, use multiple contexts to keep organized and stay sane.

Leave a comment!