If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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!
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.
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 |
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.
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.
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.
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.
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!
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?
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!
Im pretty sure that the ls command would have been developed by the fathers of Unix at Bell laboratories back in the 70s :) FWIW
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.
// 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
}
}
I think it's much better to implement a
ls
command by using theDirectoryIterator
class than to use theshell_exec()
to run the unixls
.