Practice: Find Elements, Login with 1 Step and Debug
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.
Look at the Product Admin Feature. When we built this earlier, we were planning the feature and learning how to write really nice scenarios. Now we know that most of the language we used matches the built-in definitions that Mink gives us for free.
Time to make these pass! Run just the "List available products" scenario on line 6.
To do that type, ./vendor/bin/behat
point to the file and then add :6
:
./vendor/bin/behat features/product_admin.feature:6
The number 6 is the line where the scenario starts. This prints out some missing
step definitions, so go head and copy and paste them into the FeatureContext
class:
// ... lines 1 - 76 | |
/** | |
* @Given there are :count products | |
*/ | |
public function thereAreProducts($count) | |
{ | |
// ... lines 82 - 91 | |
} | |
// ... line 93 | |
/** | |
* @When I click :linkName | |
*/ | |
public function iClick($linkName) | |
{ | |
// ... line 99 | |
} | |
/** | |
* @Then I should see :count products | |
*/ | |
public function iShouldSeeProducts($count) | |
{ | |
// ... lines 107 - 110 | |
} | |
// ... lines 112 - 129 |
Say what you need, but not more
For the thereAreProducts()
function, change the variable to count
and create a
for loop:
// ... lines 1 - 76 | |
/** | |
* @Given there are :count products | |
*/ | |
public function thereAreProducts($count) | |
{ | |
for ($i = 0; $i < $count; $i++) { | |
// ... lines 83 - 88 | |
} | |
// ... lines 90 - 91 | |
} | |
// ... lines 93 - 129 |
Inside, create some products and put some dummy data on each one:
// ... lines 1 - 81 | |
for ($i = 0; $i < $count; $i++) { | |
$product = new Product(); | |
$product->setName('Product '.$i); | |
$product->setPrice(rand(10, 1000)); | |
$product->setDescription('lorem'); | |
// ... lines 87 - 88 | |
} | |
// ... lines 90 - 129 |
Why dummy data? The definition says that we need 5 products: but it doesn't say what those products are called or how much they cost, because we don't care about that for this scenario. The point is: only include details in your scenario that you actually care about.
We'll need the entity manager in a lot of places, so create a
private function getEntityManager()
and return $this->getContainer()->get()
and pass it the service name that points directly to the entity manager:
// ... lines 1 - 120 | |
/** | |
* @return \Doctrine\ORM\EntityManager | |
*/ | |
private function getEntityManager() | |
{ | |
return $this->getContainer()->get('doctrine.orm.entity_manager'); | |
} | |
// ... lines 128 - 129 |
Perfect!
Back up in thereAreProducts()
, add $em = $this->getEntityManager();
and the usual
$em->persist($product);
and an $em->flush();
at the bottom. This is easy stuff
now that we've got Symfony booted:
// ... lines 1 - 81 | |
for ($i = 0; $i < $count; $i++) { | |
// ... lines 83 - 87 | |
$this->getEntityManager()->persist($product); | |
} | |
$this->getEntityManager()->flush(); | |
// ... lines 92 - 129 |
Using "I Click" to be more Natural
Go to the next method - iClick()
- and update the argument to $linkText
:
// ... lines 1 - 93 | |
/** | |
* @When I click :linkName | |
*/ | |
public function iClick($linkName) | |
// ... lines 98 - 129 |
We want this to work just like the built-in "I follow" function. In fact, the only reason we're not just re-using that language is that nobody talks like that: we click things.
Anyways, the built-in functionality finds the link by its text, not a CSS selector.
To use the named selector, add $this->getPage()->findLink()
, pass it $linkText
and then call click();
on that. Oh heck, let's be even lazier: just say, ->clickLink();
and be done with it:
// ... lines 1 - 98 | |
$this->getPage()->clickLink($linkName); | |
// ... lines 100 - 129 |
This looks for a link inside of page and then clicks it.
Finally, in iShouldSeeProducts()
, we're asserting that a certain number of products
are shown on the page:
// ... lines 1 - 101 | |
/** | |
* @Then I should see :count products | |
*/ | |
public function iShouldSeeProducts($count) | |
// ... lines 106 - 129 |
In other words, once we get into the Admin section, we're looking for the number of rows in the product table.
There aren't any special classes to help find this table, but there's only one
on the page, so find it via the table
class:
// ... lines 1 - 106 | |
$table = $this->getPage()->find('css', 'table.table'); | |
// ... lines 108 - 129 |
Next, use assertNotNull()
in case it doesn't exist:
// ... lines 1 - 107 | |
assertNotNull($table, 'Cannot find a table!'); | |
// ... lines 109 - 129 |
Now, use assertCount()
and pass it intval($count)
as the first argument:
// ... lines 1 - 109 | |
assertCount(intval($count), $table->findAll('css', 'tbody tr')); | |
// ... lines 111 - 129 |
For the second argument, we need to find all of the <tr>
elements inside
of the table's <tbody>
. Remember, once you find an element, you can search inside
of it with find()
or $table->findAll()
to return an array of elements instead
of just one. And don't forget that the first argument is still css
: PhpStorm is
yelling at me because I like to forget this. Ok, let's try that out!
./vendor/bin/behat features/product_admin.feature:6
Debugging Failures!
Ok, it gets further but still fails. It says:
Link "Products" not found
It's trying to find a link with the word "Products" but isn't having much luck. I wonder why? We need to debug! Right before the error, add:
And print last response
Run that one again:
./vendor/bin/behat features/product_admin.feature:6
Scroll up... up... up... all the way up to the top. Ahhh of course! We're on the login page. We forgot to login, so we're getting kicked back here.
Logging in... in one Step!
We already did all that login stuff in authentication.feature
, and I'm tempted
to copy and paste all of those lines to the top of this scenario:
// ... lines 1 - 7 | |
And I am on "/" | |
When I follow "Login" | |
And I fill in "Username" with "admin" | |
And I fill in "Password" with "admin" | |
And I press "Login" | |
// ... lines 13 - 14 |
But, it would be pretty lame to need to put all of this at the top of pretty much every scenario. You know what would be cooler? To just say:
// ... lines 1 - 6 | |
Given I am logged in as an admin | |
// ... lines 8 - 21 |
Ooo another new step definition will be needed! Rerun the test and copy the function
that behat so thoughtfully provides for us. As usual, put this in FeatureContext
:
// ... lines 1 - 112 | |
/** | |
* @Given I am logged in as an admin | |
*/ | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
// ... lines 118 - 123 | |
} | |
// ... lines 125 - 142 |
Using Mink, we'll do all the steps needed to login. First, go to the login page.
Normally you'd say $this->getSession()->visit('/login')
. But don't! Instead, wrap
/login
in a call to $this->visitPath()
:
// ... lines 1 - 115 | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
// ... lines 118 - 119 | |
$this->visitPath('/login'); | |
// ... lines 121 - 123 | |
} | |
// ... lines 125 - 142 |
This prefixes /login
- which isn't a full URL - with our base URL: http://localhost:8000
.
Once we're on the login page, we need to fill out the username and password fields
and press the button. We could find this stuff with CSS, but the named selector is
a lot easier. Say $this->getPage()->findField('Username')->setValue()
. Ah, let's
be lazier and do this all at once with fillField()
. Pass this the label for the
field - Username
- and the value to fill in:
// ... lines 1 - 115 | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
// ... lines 118 - 119 | |
$this->visitPath('/login'); | |
$this->getPage()->fillField('Username', 'admin'); | |
// ... lines 122 - 123 | |
} | |
// ... lines 125 - 142 |
But hold on: before we fill in the rest, don't we need to make sure that this user
exists in the database? Absolutely, and fortunately, we already have a function that
creates a user: thereIsAnAdminUserWithPassword()
. Call that from our function and
pass it the usual admin
/ admin
:
// ... lines 1 - 115 | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
$this->thereIsAUserWithPassword('admin', 'admin'); | |
$this->visitPath('/login'); | |
// ... lines 121 - 123 | |
} | |
// ... lines 125 - 142 |
Finish by filling in the password field and pressing the button. For that, there's
another shortcut: instead of findButton()
then press()
, use pressButton('Login')
:
// ... lines 1 - 115 | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
// ... lines 118 - 120 | |
$this->getPage()->fillField('Username', 'admin'); | |
$this->getPage()->fillField('Password', 'admin'); | |
$this->getPage()->pressButton('Login'); | |
} | |
// ... lines 125 - 142 |
This reproduces the steps from the login scenario, so that should be it! Run it!
./vendor/bin/behat features/product_admin.feature:6
We're in great shape.
Hi Ryan. I can not get to work this chapter. The products are not being created into the database, how can I debug the step that creates the products ''? The error is :
Then I should see 5 products # FeatureContext::iShouldSeeProducts()
Failed asserting that actual size 0 matches expected size 5.
So the products are not being created, I placed var_dumps and dumps on the function thereAreProducts but it doesn't get printed.
Thanks