Context Organization and Behat Suites
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
When Behat loads, it reads step definitions from FeatureContext
and MinkContext
because of the behat.yml
setup:
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:
// ... 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:
// ... 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:
// ... 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
:
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
:
default: | |
suites: | |
default: | |
// ... lines 4 - 7 | |
commands: | |
contexts: | |
- CommandLineProcessContext | |
// ... lines 11 - 18 |
Remove that from the default
suite:
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]
:
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/
:
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.
How to run default features and command features with a single line of command?