Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

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.

Leave a comment!

14
Login or Register to join the conversation
Anthony R. Avatar
Anthony R. Avatar Anthony R. | posted 5 years ago | edited

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!

Reply

Hey Anthony R.!

Hmmm, interesting. What driver are you using? Goutte? Selenium? Or the Symfony driver? My first thought is that, unless you're using the Symfony driver, this should not happen. The reason is that, if you use something like Goutte or Selenium that makes real HTTP requests, the kernel inside your *Context files is booted, and remains booted throughout the entire test suite. So the shutdown should never happen. But if you're using the Symfony driver, then yea, it will shutdown and reboot the kernel between each request - you would indeed likely be in this boat!

I do have one idea to get around it: store the $currentUserId instead. Then, add a getCurrentUser() method that always uses that value to query for a fresh object from the EntityManager. That should guarantee it's always fresh :).

Cheers!

1 Reply
Anthony R. Avatar

Ryan! This is exactly right, I was using Symfony driver and I have now switched to Goutte driver - thank you! You are a legend! Greetings to you and the team from Australia.

Reply

Yo Anthony R.!

Woohoo! Greetings back to Australia from us (Michigan USA, Mexico and Ukraine) :D

Cheers!

Reply
Dan_M Avatar

Hey guys! I've been following along with tests against my current project, and I've run into a snag. When I manipulate the database in my FeatureContext.php file, I am affecting the database defined in my config_test.yml. But when I execute the tests, I'm hitting the database defined in config.yml (there is no database definition in config_dev.yml). I assume that's because behat is running in the test environment and the tests are hitting my built in server that's running in the dev environment.

I'd like to test against a database that is emptied and configured specifically for each test (as you recommend), but I'd like to do that without blowing out my dev database. Is there a way to do that, or is that simply a bad idea?

Thanks!

Reply

Hey Dan_M

Working in isolation with a dedicated Database is the *way* to do your testing. I believe you have something misconfigured.
Are you using the Symfony2 extension?
Can you show me your behat.yml file?

Cheers!

Reply
Dan_M Avatar
Dan_M Avatar Dan_M | MolloKhan | posted 5 years ago | edited

Yes, I am using the Symfony2 extension.

My behat.yml is pretty basic:


default:
    extensions:
        Behat\MinkExtension:
            base_url: http://localhost:8000
            browser_name: 'chrome'
            goutte: ~
            selenium2: ~

        Behat\Symfony2Extension:

    suites:
        default:
            contexts:
                - FeatureContext
                - Behat\MinkExtension\Context\MinkContext

For what it's worth, I'm using the following:
behat 3.4.1
symfony 3.3.10

Reply
Dan_M Avatar
Dan_M Avatar Dan_M | MolloKhan | posted 5 years ago | edited

Aha! I get it. I installed behat/mink and behat/mink-browserkit-driver and updated my behat.yml to the following:


default:
    extensions:
        Behat\Symfony2Extension: ~
        Behat\MinkExtension:
            base_url: http://localhost:8000
            browser_name: 'chrome'
            goutte: ~
            selenium2: ~
            sessions:
                default:
                    symfony2: ~
    suites:
        default:
            contexts:
                - FeatureContext
                - Behat\MinkExtension\Context\MinkContext

Now behat runs against the server in the test environment without my having to start the server.

Thanks!

Reply
Dan_M Avatar

Hmmm...with this behat.yml, my @javascript annotations still want to run on the built-in server (dev environment) instead of using the symfony2 extension server in the test mode. What am I missing?

Reply

Yo Dan_M!

Ok, let's talk about a few possibilities :). First, using the symfony2 "session" is fine - this is how you solved that pesky "different databases" problem - nice work! A different solution to that problem (and one you will probably need to do) is to add a new front controller - e.g. app_test.php that is identical to app_dev.php except that it boots the AppKernel() in the test environment. Then, you should be able to update your base_url to http://localhost:8000/app_test.php.

Next issue: why does adding @javascript not cause the selenium session to be used? I'm not sure... but try changing the annotation to @mink:selenium2 - the @javascript annotation is "sort of" short for this. We can see if that makes any difference. I've actually not run into this issue yet... so it must be something minor...

Cheers!

Reply
Dan_M Avatar

I set up app_test.php, started the selenium driver, started the server with "console --env=test server:run", then ran behat against a scenario with @javascript, and everything went according to plan.

When I use either @javascript or @mink:selenium2 without first staring the server, the browser opens but I get a site failed to connect error. (I'm on Windows 10, by the way.)

All in all, that's OK. I have a way to start the server in test mode and run javascript tests against it.

Thanks!

Reply

Hey Dan_M

I'm glad we could figure it out!
One more thing, I think you don't have to start your server in test mode in order to run your tests, try starting it as normal and try again. If I'm wrong, let me know :)

Have a nice day

Reply
Dan_M Avatar
Dan_M Avatar Dan_M | MolloKhan | posted 5 years ago | edited

Hey MolloKhan

You're right, of course. When the selenium driver runs, it goes to the url given as base_url in the config. Because that's http://localhost:8000/app_test.php, it hits the test database.

Thanks!

1 Reply

Besides Ryan's comment, don't forget that you need to have Selenium server running whenever you want to use @javascript annotation

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a very old version of Symfony. The fundamentals of Behat are still valid, but integration with Symfony will be different.

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice