Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Behat

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

Ok, the basics of Gherkin, writing features and scenarios, are now behind us. Now, how does this "Behat" thing fit in?

Imagine we've gone back in time 25 years and Linus Torvalds, the Yoda of Linux, comes to us and says:

I would love your help in building the ls command.

Yep, the ls command right here in the terminal. Since you're an awesome dev, you reply:

I would be happy to help you Linus, and I'll use Gherkin to describe the feature and the scenarios for the ls command. I'm really into Behavior Driven Development.

Writing Scenario #1

Create a new ls.feature file:

Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
... lines 5 - 12

I'll save some time by copying the feature description in since we've already covered this pretty well. On to the scenario! The first scenario might be:

... lines 1 - 5
Scenario: List 2 files in a directory
... lines 7 - 12

And now we'll go through our Given, When, and Then lines. Since we need to list two files we'll need to create those first:

... lines 1 - 6
Given I have a file named "john"
And I have a file named "hammond"
... lines 9 - 12

The user action would be actually running the ls command:

... lines 1 - 8
When I run "ls"
... lines 10 - 12

Finally, we'll actually test that the "john" and "hammond" files both appear in the output:

... lines 1 - 9
Then I should see "john" in the output
And I should see "hammond" in the output

Writing this scenario has two purposes. First, it lets us plan how our feature should behave. And that's what we've been talking about. Second, we want the scenario to be executed as a test to prove whether or not we have successfully created this behavior. To do that, we'll run ./vendor/bin/behat in our terminal and point it at features/ls.feature and let's see what happens!

The Essence of Behat: Matching Scenario lines with Functions

Ahh, so it says our scenario was undefined and something about our FeatureContext having missing steps. And it even gives us some PHP code. Copy these three functions, open up the FeatureContext class that we generated and paste them there:

... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 24
/**
* @Given I have a file named :arg1
*/
public function iHaveAFileNamed($arg1)
{
throw new PendingException();
}
/**
* @When I run :arg1
*/
public function iRun($arg1)
{
throw new PendingException();
}
/**
* @Then I should see :arg1 in the output
*/
public function iShouldSeeInTheOutput($arg1)
{
throw new PendingException();
}
}

So what does Behat really do? Simply, it reads each line of our scenario, looks for a matching function inside of FeatureContext and calls it. In this case, it'll read "There is a file named "john", find that it matches this annotation here and then execute its function.

What's really cool is that because we surrounded john with quotes, it recognized that as a wildcard. In the annotation, we have :arg1 which means it matches anything surrounded in quotes or a number.

Filling in the Definitions/Functions

Our job is just to make these functions do what they say they will do. I'll change this to :filename and update the argument to $filename, just because that's more descriptive:

... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 24
/**
* @Given I have a file named :filename
*/
public function iHaveAFileNamed($filename)
... lines 29 - 47
}

In this function, how do we create a file? How about we use touch($filename);:

... lines 1 - 29
touch($filename);
... lines 31 - 49

For iRun() update the arg1's to command. There are lots of ways to run commands, but we'll use shell_exec($command);:

... lines 1 - 32
/**
* @When I run :command
*/
public function iRun($command)
{
shell_exec($command);
}
... lines 40 - 49

Sharing Data inside your Scenario (between "Steps")

Lastly, in iShouldSeeInTheOutput(), update the arg1 to string:

... lines 1 - 42
/**
* @Then I should see :string in the output
*/
public function iShouldSeeInTheOutput($string)
... lines 47 - 53

And now we're stuck... we don't have the return value from shell_exec() above. Good news, there is a really nice trick for this. Whenever you need to share data between functions in FeatureContext, you'll just create a new private property. At the top of the class let's create a private $output property and update the iRun function to $this->output = shell_exec($command);:

... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
private $output;
... lines 15 - 37
public function iRun($command)
{
$this->output = shell_exec($command);
}
... lines 42 - 51
}

Behat doesn't care about the new property: this is just us being good object oriented programmers and sharing things inside a class.

Lifetime of a Scenario

This works because every scenario gets its own FeatureContext object. When we have more scenarios later, Behat will instantiate a fresh FeatureContext object before calling each one. So we can set any private properties that we want on top, and only just this scenario will have access to it.

Failing!

Now in iShouldSeeInTheOutput() method if (strpos($this->output, $string) === false)) then we have a problem and we want this step to fail:

... lines 1 - 45
public function iShouldSeeInTheOutput($string)
{
if (strpos($this->output, $string) === false) {
... line 49
}
}
... lines 52 - 53

How do you fail in Behat? By throwing an exception: throw new \Exception() and print a really nice message here of "Did not see '%s' in the output '%s'. Finish that line up with $string, $this->output:

... lines 1 - 48
throw new \Exception(sprintf('Did not see "%s" in output "%s"', $string, $this->output));
... lines 50 - 53

Ok let's give this a try!

Re-run our last Behat command in the terminal, and this time it's green! And if you run the ls command here you can see the "john" and "hammond" files listed.

What are Definitions and Steps?

Back to our scenario. Get out some pen and paper: we need to review some terminology! Every line in here is called a "step":

... lines 1 - 6
Given I have a file named "john"
And I have a file named "hammond"
When I run "ls"
Then I should see "john" in the output
And I should see "hammond" in the output

And the function that a step connects to is called a "step definition":

... lines 1 - 11
class FeatureContext extends MinkContext implements Context, SnippetAcceptingContext
{
... lines 14 - 26
/**
* @Given I have a file named :filename
*/
public function iHaveAFileNamed($filename)
... lines 31 - 34
/**
* @When I run :command
*/
public function iRun($command)
... lines 39 - 42
/**
* @Then I should see :string in the output
*/
public function iShouldSeeInTheOutput($string)
... lines 47 - 51
}

This is important in helping you understand Behat's documentation.

Oh, and the 4 feature lines above: those aren't parsed by Behat:

Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
... lines 5 - 12

We only write those to go through the exercise of thinking about our business value.

Now, you see in our test it says "5 steps (5 passed)", which means that each step could fail. The rule is really simple, if the definition function for a step throws an exception, it's a failure. If there's no exception it passes.

Head back to the step that looks for the "hammond" file and add the number 2 to the end of the file name so it fails. Running the scenario in our terminal now shows us 4 steps passed and 1 failed.

Leave a comment!

8
Login or Register to join the conversation

I think it's much better to implement a ls command by using the DirectoryIterator class than to use the shell_exec() to run the unix ls.

Reply

Hey ahmadmayahi!

Yea, I'd agree actually :). This is just an example, and the point is to actually test the ls command itself. But if I really wanted directory info in PHP, I'd totally not do it this way. Actually, I usually use Symfony's Finder component - it's got a few extra nice features.

Cheers!

Reply
Default user avatar
Default user avatar Muhammad Taqi Hassan | posted 5 years ago

You show that we can see how many steps are passed and failed. Is there any quick way to know which step is failed and detail reason for that?

Reply

Hey Muhammad!

Yes! Well, probably :). The Behat executable has a number of different output options, so that the final output can be more or less descriptive. By default, it summarizes (at the bottom) how many steps passed/failed, and you can scroll up to see which steps fails with the detailed reason (well, semi-detailed: Behat only knows which *step* failed, and the error message that was thrown that caused the failure). By using the the --format (or just -f) flag, you can change the output: http://docs.behat.org/en/v2...

Alternatively, you could use a hook to do some custom logging on each failure - see https://knpuniversity.com/s...

Let me know if that helps!

Cheers!

Reply
Default user avatar
Default user avatar lindsay macvean | posted 5 years ago

Im pretty sure that the ls command would have been developed by the fathers of Unix at Bell laboratories back in the 70s :) FWIW

Reply

Ah damn, that is almost definitely true :).

Reply
Default user avatar

This comment is for the previous "Challenge 2 of 2" (https://knpuniversity.com/s... there is a quote (") missing after "Given there is a" in the Scenario box.

Reply

Thanks AJ! I got that quote added now!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a very old version of Symfony. The fundamentals of Behat are still valid, but integration with Symfony will be different.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.4.0, <7.3.0",
        "symfony/symfony": "^2.7", // v2.7.4
        "twig/twig": "^1.22", // v1.22.1
        "sensio/framework-extra-bundle": "^3.0", // v3.0.16
        "doctrine/doctrine-bundle": "^1.5", // v1.5.1
        "doctrine/orm": "^2.5", // v2.5.1
        "doctrine/doctrine-fixtures-bundle": "^2.2", // v2.2.1
        "behat/symfony2-extension": "^2.0" // v2.0.0
    },
    "require-dev": {
        "behat/mink-extension": "^2.0", // v2.0.1
        "behat/mink-goutte-driver": "^1.1", // v1.1.0
        "behat/mink-selenium2-driver": "^1.2", // v1.2.0
        "phpunit/phpunit": "^4.8" // 4.8.18
    }
}
userVoice