Buy

Behat experts coming through! Seriously, we've covered it: Behat reads the steps, finds a function using this nice little pattern, calls that function, and then goes out to lunch. That's all that Behat does, plus a few nice extras.

Let's dive into some of those extras! This scenario creates the "john" and "hammond" files inside this directory but doesn't even clean up afterwards. What a terrible roommate.

Let's first put these into a temporary directory. We'll use the __construct function because that's called before each scenario. Type mkdir('test'); and chdir('test');:

55 lines features/bootstrap/FeatureContext.php
... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 22
public function __construct()
{
mkdir('test');
chdir('test');
}
... lines 28 - 53
}

Over in the terminal, delete the "john" and "hammond" files so we can have a fresh start at this. Rerun Behat for our ls scenario:

vendor/bin/behat features/ls.feature

Everything still passes and hey look there's a little test/ directory and the "john" and "hammond" are inside of that. Cool.

Ready for the problem? Rerun that test one more time. Now, errors show up that say:

mkdir(): file exists.

This error didn't break our test but it does highlight the problem that we don't have any cleanup. After our tests run these files stick around.

We need to run some code after every scenario. Behat has a system called "hooks" where you can make a function inside of your context and tell Behat to call it before or after your scenario, entire test suite or individual steps.

Create a new public function inside called public function moveOutOfTestDir():

75 lines features/bootstrap/FeatureContext.php
... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 66
public function moveOutOfTestDir()
{
... lines 69 - 72
}
}

This will be our cleanup function. Use chdir('..'); to go up one directory. Then, if the test/ directory exists - which it should - then we'll run a command to remove that:

75 lines features/bootstrap/FeatureContext.php
... lines 1 - 68
chdir('..');
if (is_dir('test')) {
system('rm -r '.realpath('test'));
}
... lines 73 - 75

To get Behat to actually call this after every scenario, add an @AfterScenario annotation above the method:

75 lines features/bootstrap/FeatureContext.php
... lines 1 - 63
/**
* @AfterScenario
*/
public function moveOutOfTestDir()
... lines 68 - 75

That's it!

Let's give this a try:

vendor/bin/behat features/ls.feature

The first time we run this we still get the warning since our clean up function hasn't been called yet. But when we run it again, the warnings are gone! And if we run ls, we see that there is no test directory.

We can do this same thing with the mkdir(); and chdir(); stuff. Create a new public function moveIntoTestDir():

75 lines features/bootstrap/FeatureContext.php
... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 55
public function moveIntoTestDir()
{
... lines 58 - 61
}
... lines 63 - 73
}

And we can make it even a bit more resistant by checking to see if the test directory is already there and only create it if we need to. Above this, add @BeforeScenario:

75 lines features/bootstrap/FeatureContext.php
... lines 1 - 52
/**
* @BeforeScenario
*/
public function moveIntoTestDir()
{
if (!is_dir('test')) {
mkdir('test');
}
chdir('test');
}
... lines 63 - 75

This is basically the same as putting the code in __construct(), but with some subtle differences. @BeforeScenario is the proper way to do this.

When we run things now, everything looks really nice. I think this ls command is going to be a success!

PHPUnit Assert Functions

So bonus feature #1 is the hook system. And bonus feature #2, has nothing to do with Behat at all. It actually comes from PHPUnit. Our first step will be to install PHPUnit with composer require phpunit/phpunit --dev. That will add it under a new require-dev section in composer.json:

29 lines composer.json
{
... lines 2 - 21
"require-dev": {
... lines 23 - 25
"phpunit/phpunit": "^4.8"
}
}

Full disclosure, I should have put all the Behat and Mink stuff inside of the require-dev too: it is a better place for it since we only need them while we're developing.

I installed PHPUnit because it has really nice assert functions that we can get a hold of. To get access to them we just need to add a require statement in our FeatureContext.php file, require_once then count up a couple of directories and find vendor/phpunit/phpunit/src/Framework/Assert/Functions.php:

79 lines features/bootstrap/FeatureContext.php
... lines 1 - 8
require_once __DIR__.'/../../vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';
... lines 10 - 79

Requiring this file gives you access to all of PHPUnit's assert functions as flat functions. Down in the iShouldSeeInTheOutput() method, use assertContains(), give it the needle which is $string and the haystack which is $this->output. Finally, add our helpful message which I'll just cut and paste. Remove the rest of the original if statement:

79 lines features/bootstrap/FeatureContext.php
... lines 1 - 13
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 16 - 44
/**
* @Then I should see :string in the output
*/
public function iShouldSeeInTheOutput($string)
{
assertContains(
$string,
$this->output,
sprintf('Did not see "%s" in output "%s"', $string, $this->output)
);
}
... lines 56 - 77
}

Run the test again!

vendor/bin/behat features/ls.feature

Beautiful, it looks just like it did before.

Using Background

To show you the final important extra for Behat, create another scenario for Linus' ls feature. This time we'll say:

19 lines features/ls.feature
... lines 1 - 12
Scenario: List 1 file and 1 directory
... lines 14 - 19

I'll copy all the steps from our first scenario and just edit the second line to:

19 lines features/ls.feature
... lines 1 - 13
Given I have a file named "john"
And I have a dir named "ingen"
When I run "ls"
Then I should see "john" in the output
... lines 18 - 19

And update the final line to:

19 lines features/ls.feature
... lines 1 - 17
And I should see "ingen" in the output

Man what a great looking scenario, let's run it!

vendor/bin/behat features/ls.feature

As expected it now says there's one missing step definition. Copy the PHP code that prints out into FeatureContext. Remove the throw exception line, and update the arg1's to dir:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 13
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 16 - 78
/**
* @Given I have a dir named :dir
*/
public function iHaveADirNamed($dir)
{
... line 84
}
}

Inside the function use mkdir($dir) to actually make that directory:

87 lines features/bootstrap/FeatureContext.php
... lines 1 - 83
mkdir($dir);
... lines 85 - 87

Simple!

Back to the terminal to rerun the tests. It works! And that was easy. Once you're done celebrating you may start to notice the duplication we have in the scenarios. There are two ways to clean this up. The most important way is with Background::

20 lines features/ls.feature
... lines 1 - 5
Background:
Given I have a file named "john"
... lines 8 - 20

If every single scenario in your feature starts with the same lines then you should move that up into a new Background section.

Now, I'll change the first line of And in each of these scenarios to Given:

20 lines features/ls.feature
... lines 1 - 8
Scenario: List 2 files in a directory
Given I have a file named "hammond"
... lines 11 - 13
Scenario: List 1 file and 1 directory
Given I have a dir named "ingen"
... lines 17 - 20

I don't have to do this, but it reads better to me. Now Behat will run that Background line before each individual scenario and you'll even see that:

vendor/bin/behat features/ls.feature

The Background is read up here, but it actually is running before the top scenario and the bottom one. We know this because if it didn't, these tests wouldn't be passing.

Second, when you have duplication that's not on the first line of all of your scenarios like the "Then I should see...." you may want to use scenario outlines. It's a little less commonly used but we'll dive into that a bit later.

Ok, not only do you know how Behat works but you even know all of its top extra features -- check you out!

Leave a comment!

  • 2016-05-02 weaverryan

    Hi Florian!

    Ah, it's because system() ultimately runs a command-line script. If you think about it, the two commands would be:


    rm -r /path/to/test

    rm -r/path/to/test

    If there's no space, the -r flag "runs into" the path :).

    Cheers!

  • 2016-04-30 Florian

    Hey,
    do you know why

    //Works
    if (is_dir('test')){
    system('rm -r '.realpath('test'));
    }

    works but

    //Don't work
    if (is_dir('test')){
    system('rm -r'.realpath('test'));
    }

    not? The difference is, there is a space between -r and '. Works -> 'rm -r ' | Don't work 'rm -r' This is strange.

  • 2016-04-19 weaverryan

    Awesome! DrupalCon is one of my absolute favorite conferences - incredibly well-run and great people. See you there!

  • 2016-04-15 Daniel Noyola

    Yes! First DrupalCon ever :)

  • 2016-04-15 weaverryan

    Haha, let's say I was ;). Will you be there?

  • 2016-04-13 Daniel Noyola

    Cool! this is getting more amazing!

    P.S. it's okay as long you were working on this https://events.drupal.org/newo... or this https://events.drupal.org/newo...

  • 2016-04-13 weaverryan

    Hey Daniel!

    Inside FeatureContext, you *do* have access to any of your normal classes (e.g. anything in the vendor directory, etc). So, you should be able to use Guzzle without needing any require statements - and actually, we do this in the REST tutorial (http://knpuniversity.com/scree... or https://github.com/knpuniversi... to see usage - though this is an older version of Guzzle now).

    We added the require_once in this case to get the PHPUnit *functions* - as function autoloading is a bit different. However, this may not be needed anymore even for functions - I believe PHPUnit takes care of autoloading these automatically now as well.

    I hope that helps!

    P.S. Sorry for the slow reply!

  • 2016-04-05 Daniel Noyola

    is there a way to use the "use" sentence and avoid the "include_once"? it just feels ugly. what I should add to the featureContext class if I want to test an API using Guzzle?