When *I* do Something: Handling the Current User
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.
Time for a challenge! Whenever you have products in the admin area, it either shows
the name of the user that created it - like admin
- or anonymous
if it was created
via some other method without an author. Right now, our admin area lists "anonymous"
next to every product. The reason is simple: we're not setting the author when we
create the products in FeatureContext
.
I want to test that this table does show the correct author when its set. Create a new scenario to describe this:
// ... lines 1 - 12 | |
Scenario: Products show owner | |
Given I am logged in as an admin | |
// ... lines 15 - 28 |
Instead of just saying there are five products I'll say:
// ... lines 1 - 14 | |
And I author 5 products | |
// ... lines 16 - 28 |
This is new language that will need a step definition. To save time, we can go directly to the products page:
// ... lines 1 - 15 | |
When I go to "/admin/products" | |
# no products will be anonymous | |
Then I should not see "Anonymous" | |
// ... lines 19 - 28 |
Since "I" - some admin user - will be the author of the products, they should all show "admin": none will say "Anonymous". And we will only have these 5 products because we're clearing the database between each scenario to keep things independent.
Run just this new scenario by using its line number:
./vendor/bin/behat features/web/product_admin.feature:13
Great - copy the iAuthorProducts()
function code and paste it into our handy FeatureContext
class - near the other product function:
// ... lines 1 - 85 | |
/** | |
* @Given I author :count products | |
*/ | |
public function iAuthorProducts($count) | |
{ | |
// ... line 91 | |
} | |
// ... lines 93 - 160 |
These two functions will be similar, so we should reuse the logic. Copy the internals
of thereAreProducts
, make a new private function createProducts()
. Pass it $count
as an argument and also an optional User object which will be the author for those products:
// ... lines 1 - 141 | |
private function createProducts($count, User $author = null) | |
{ | |
for ($i = 0; $i < $count; $i++) { | |
$product = new Product(); | |
$product->setName('Product '.$i); | |
$product->setPrice(rand(10, 1000)); | |
$product->setDescription('lorem'); | |
// ... lines 149 - 153 | |
$this->getEntityManager()->persist($product); | |
} | |
$this->getEntityManager()->flush(); | |
} | |
// ... lines 159 - 160 |
Now, add an if statement that says, if $author
is passed then, $product->setAuthor()
:
// ... lines 1 - 149 | |
if ($author) { | |
$product->setAuthor($author); | |
} | |
// ... lines 153 - 160 |
I already have that relationship setup with in Doctrine. Great!
In thereAreProducts()
, change the body of this function to $this->createProducts($count);
:
// ... lines 1 - 80 | |
public function thereAreProducts($count) | |
{ | |
$this->createProducts($count); | |
} | |
// ... lines 85 - 160 |
Do the same thing in iAuthorProducts()
for now:
// ... lines 1 - 88 | |
public function iAuthorProducts($count) | |
{ | |
$this->createProducts($count); | |
} | |
// ... lines 93 - 160 |
Clearly, this is still not setting the author. But I want to see if it executes first and then we'll worry about setting the author.
Who is "I" in a Scenario?
Cool! It runs... and fails because anonymous is still shown on the page. The question
now is: how do we get the current user? The step says "I author". But who is "I" in this
case? In product_admin.feature
:
// ... lines 1 - 5 | |
Scenario: List available products | |
Given I am logged in as an admin | |
// ... lines 8 - 11 | |
Scenario: Products show owner | |
Given I am logged in as an admin | |
// ... lines 15 - 28 |
You can see that "I" is whomever we logged in as. We didn't specify what the username should be for that user, but whoever is logged in is who "I" represents.
When we worked with the ls
scenarios earlier, we needed to share the command output
string between the steps of a scenario. In this case, we have a similar need: we
need to share the user object from the step where we log in, with the step where
"I" author some products. To share data between steps, create a new private $currentUser;
:
// ... lines 1 - 16 | |
class FeatureContext extends RawMinkContext implements Context, SnippetAcceptingContext | |
{ | |
// ... lines 19 - 20 | |
private $currentUser; | |
// ... lines 22 - 162 | |
} |
In iAmLoggedInAsAnAdmin()
, add $this->currentUser = $this->thereIsAnAdminUserWithPassword()
:
// ... lines 1 - 119 | |
public function iAmLoggedInAsAnAdmin() | |
{ | |
$this->currentUser = $this->thereIsAUserWithPassword('admin', 'admin'); | |
// ... lines 123 - 127 | |
} | |
// ... lines 129 - 164 |
Click to open that function. It creates the User
object of course, but now we need
to also make sure it returns that:
// ... lines 1 - 42 | |
/** | |
* @Given there is an admin user :username with password :password | |
*/ | |
public function thereIsAnAdminUserWithPassword($username, $password) | |
{ | |
// ... lines 48 - 56 | |
return $user; | |
} | |
// ... lines 59 - 164 |
And that's it! This login step will cause the currentUser
property to be set and
in iAuthorProducts()
we can access that and pass it into createProducts()
so that
each product us authored by us:
// ... lines 1 - 89 | |
/** | |
* @Given I author :count products | |
*/ | |
public function iAuthorProducts($count) | |
{ | |
$this->createProducts($count, $this->currentUser); | |
} | |
// ... lines 97 - 164 |
It's pretty common to want to know who is logged in, so you'll likely want to use this in your project.
And hey it even passes! Now you can continue to write scenarios in terms of actions that "I" take and we will actually know who "I" is.
there Thank you Ryan for another outstanding post.
I have noticed that saving the authenticated user in `$currentUser` may become an issue if the test involves clicking on links.
The currentUser would become unmanaged after requesting another page as the kernel is being shut down every time and the entity managers are clear()ed... This is since the following commit in Doctrine Bundle.
https://github.com/doctrine...
This issue is being discussed here:
https://github.com/doctrine...
Not sure if you have a current way of getting around this, but just thought some people might run into the same issue.
Thank you for your outstanding posts!