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 |
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 |
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
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.
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.
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!
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!
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!
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
// 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
}
}
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