Buy

Gherkin Tables: Given I have the following:

To show off a nice Behat feature, add a line to our scenario:

35 lines features/web/product_admin.feature
... 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:

199 lines features/bootstrap/FeatureContext.php
... 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:

35 lines features/web/product_admin.feature
... 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:

35 lines features/web/product_admin.feature
... 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!

43 lines features/web/product_admin.feature
... 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:

43 lines features/web/product_admin.feature
... 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:

43 lines features/web/product_admin.feature
... lines 1 - 22
Given the following products exist:
| name | is published |
| Foo1 | yes |
| Foo2 | no |
... lines 27 - 43

Ok, keep going on the scenario:

43 lines features/web/product_admin.feature
... 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:

209 lines features/bootstrap/FeatureContext.php
... 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:

209 lines features/bootstrap/FeatureContext.php
... 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:

220 lines features/bootstrap/FeatureContext.php
... 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:

220 lines features/bootstrap/FeatureContext.php
... 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.

Leave a comment!