Buy

Practice: Find Elements, Login with 1 Step and Debug

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:

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

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

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

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

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

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

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

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

129 lines features/bootstrap/FeatureContext.php
... lines 1 - 106
$table = $this->getPage()->find('css', 'table.table');
... lines 108 - 129

Next, use assertNotNull() in case it doesn't exist:

129 lines features/bootstrap/FeatureContext.php
... lines 1 - 107
assertNotNull($table, 'Cannot find a table!');
... lines 109 - 129

Now, use assertCount() and pass it intval($count) as the first argument:

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

14 lines features/web/authentication.feature
... 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:

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

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

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

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

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

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

Leave a comment!

  • 2016-02-15 Daniel

    Thanks a lot Ryan! It did work, I haven't notice the autoload.php inside the app directory. I tried the vendor autoload with no luck. I am all set!!
    Cheers

  • 2016-02-11 weaverryan

    Ah, or just include vendor/autoload.php - if you're outside of Symfony :)

  • 2016-02-11 weaverryan

    Hey Daniel!

    Great - glad you're on good ground again :). About the play file - just require the app/autoload.php file instead of bootstrap.php.cache. The bootstrap file does the same thing as autoload, but gives you a small (and optional) performance boost in production.

    Cheers!

  • 2016-02-10 Daniel

    Hi Ryan! Finally got the error, it was just a type in the for loop... I don not know where to hide myself. Anyway I learnt a lot thanks to your indications. There is only one thing I wasn't able to do and that is to create a play file on the root folder, this project does not have a bootstrap.php.cache.
    Cheers!

  • 2016-01-13 weaverryan

    Hey Daniel!

    Hmm, so you're definitely going down the right path for debugging. This is what I would try:

    A) At the end of thereAreProducts, put a die statement. Now, go look in your database. Are the products there or is the products table empty? If it's empty, you at least know there is a problem with iShouldSeeProducts(). If not, continue.
    B) Remove the die from thereAreProducts and add an "And I break" right before the failing step (Then i should see 5...). Make sure you have an @javascript above the scenario, at least temporarily. Run the scenario. When it stops, do you see 5 products on the page? If you do, then you know that we have a bug in our code that looks for the 5 products (e.g. we're using the wrong CSS class name to select something). If you don't see the products (but know that they are in the database, from [A] above), then it's possible that you're making web requests into Symfony's "dev" environment, but loading all of the products in the "test" environment (and if you have a separate database for those environments, that would cause the problem).

    Let me know what you find out!

  • 2016-01-11 Daniel

    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