Gherkin Tables: Given I have the following:
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.
To show off a nice Behat feature, add a line to our scenario:
// ... lines 1 - 5 | |
Scenario: List available products | |
// ... line 7 | |
And there are 5 products | |
And there is 1 product | |
// ... lines 10 - 35 |
We now have one step that adds 5 products and another - almost identical that adds one more product. But, the language isn't quite the same, so PhpStorm highlights it as an undefined step. How can we use this language but have it re-use the definition we already have?
One trick is to add a second annotation statement to that definition. And that would make it work. But there's a better way that's new to Behat3: conditional language.
Using this/that Conditional Language
Update the annotation to There is/are :count product(s)
with the 's' inside of
parentheses:
// ... lines 1 - 16 | |
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext | |
{ | |
// ... lines 19 - 81 | |
/** | |
* @Given there is/are :count product(s) | |
*/ | |
public function thereAreProducts($count) | |
// ... lines 86 - 197 | |
} |
Now, change the end of the scenario to look for 6 products:
// ... lines 1 - 5 | |
Scenario: List available products | |
// ... lines 7 - 11 | |
Then I should see 6 products | |
// ... lines 13 - 35 |
Run just this scenario:
./vendor/bin/behat features/product_admin.feature:6
The new language matches both steps and we're passing. While we're here, add
a proper Background
and move the login step there. Remove the duplicated line from
each scenario:
// ... lines 1 - 5 | |
Background: | |
Given I am logged in as an admin | |
Scenario: List available products | |
Given there are 5 products | |
// ... lines 11 - 15 | |
Scenario: Products show owner | |
Given I author 5 products | |
// ... lines 18 - 22 | |
Scenario: Add a new product | |
Given I am on "/admin/products" | |
// ... lines 25 - 35 |
Using Gherkin TableNodes
Next, I want to add a test for this "Is published" flag: if a product is not published,
it has a little x
icon. If it is published it has a ✓
. I want to make sure
these are showing up correctly. Right now, all of the products are unpublished.
Woo! Time for a new scenario!
// ... lines 1 - 21 | |
Scenario: Show published/unpublished | |
// ... lines 23 - 43 |
But this time, we can't just say "Given 5 products exist" because we need to control the published flag. But we can use a new trick, add:
// ... lines 1 - 22 | |
Given the following products exist: | |
// ... lines 24 - 43 |
End the line with a colon and below, build a table just like we did earlier with scenario outlines. Give it two headers: "name" and "is published": I'm making these headers up: you'll see how I use them in a second. Call the first product "Foo1" and make it published. Call the second "Foo2" and make it not published:
// ... lines 1 - 22 | |
Given the following products exist: | |
| name | is published | | |
| Foo1 | yes | | |
| Foo2 | no | | |
// ... lines 27 - 43 |
Ok, keep going on the scenario:
// ... lines 1 - 26 | |
When I go to "/admin/products" | |
# todo | |
// ... lines 29 - 43 |
I'll stop here for now and add the missing Then
line that looks for the published
flag later. Try this by running only this scenario:
./vendor/bin/behat features/product_admin.feature:21
Copy the new function into FeatureContext
:
// ... lines 1 - 97 | |
/** | |
* @Given the following products exist: | |
*/ | |
public function theFollowingProductsExist(TableNode $table) | |
{ | |
// ... lines 103 - 105 | |
} | |
// ... lines 107 - 209 |
The TableNode Object
Ah, but this looks different: it saw that the step had a table below it and passed
us a TableNode
object that represents the data in the table after it. Let's iterate
over the object and dump each row out to see what happens:
// ... lines 1 - 100 | |
public function theFollowingProductsExist(TableNode $table) | |
{ | |
foreach ($table as $row) { | |
var_dump($row); | |
} | |
} | |
// ... lines 107 - 209 |
Science! Rerun the test:
./vendor/bin/behat features/product_admin.feature:21
Each row is printed as an associative array using the header and the value for that row. How cool is that?
To save some time, I'll copy some code that creates Product
objects:
// ... lines 1 - 102 | |
foreach ($table as $row) { | |
$product = new Product(); | |
$product->setName($row['name']); | |
$product->setPrice(rand(10, 1000)); | |
$product->setDescription('lorem'); | |
if ($row['is published'] == 'yes') { | |
$product->setIsPublished(true); | |
} | |
$this->getEntityManager()->persist($product); | |
} | |
// ... lines 115 - 220 |
This adds some duplication to my FeatureContext
- shame on me. In a real project,
I want you to be a little more careful.
In this scenario, we won't give each product an author because we don't need that
for what we're testing. Call setName()
passing it $row['name']
. Then
if ($row['is published'] == 'yes')
, add $product->setIsPublished(true);
.
The is published
with a space in the middle is on purpose: I want a human to be
able to read the scenarios. And other than super geeks like you and I, is published
reads a lot better than is_published
. And, "yes" for published is more expressive
than putting a 1 or 0. In FeatureContext
, we translate all of that to code.
Fetch the entity manager and flush the changes at the bottom:
// ... lines 1 - 115 | |
$this->getEntityManager()->flush(); | |
// ... lines 117 - 220 |
Great!
Cool this passes! But don't get too excited: we don't have a Then
statement yet.