Handle authenticated users in Behat/Mink
One useful addition to PHP's testing tools is Behat. It focuses on business expectations through behavior driven development, while PHPUnit is usually used for lower-level test driven development. With BDD, you describe how the feature should behave from the user's point of view.
If you use Behat with Mink, you can test browser behavior. But when your application has authentication, you probably do not want to fill in the login form in every scenario. You do not have to.
First, look at this Behat feature:
gherkinFeature: Sign in to the websiteIn order to access the administrative interfaceAs a visitorI need to be able to log in to the websiteBackground:Given there are following users:| username | email | plain_password | enabled || bar | [email protected] | foo | yes |Scenario: Log in with username and passwordGiven I am on "/admin/login"When I fill in the following:| username | bar || password | foo |And I press "Login"Then I should be on "/admin/"And I should see "Logout"Scenario: Log in with bad credentialsGiven I am on "/admin/login"When I fill in the following:| username | [email protected] || password | bar |And I press "Login"Then I should be on "/admin/login"And I should see "Invalid username or password"
This feature describes a login form. It creates a bar user, logs in with valid credentials, then checks the failed login path. But what if you want to test an admin dashboard? You might start with something like this:
gherkinFeature: Use the admin dashboardIn order to see the current status of my applicationAs an administrative userI need to be able to use the admin dashboardBackground:Given there are following users:| username | email | plain_password | enabled || bar | [email protected] | foo | yes |Scenario: Displaying the blog overviewGiven I am on "/admin/login"When I fill in the following:| username | bar || password | foo |And I press "Login"Then I should be on "/admin/"And I should see "Admin dashboard"
This scenario fills in the login form just to reach the admin dashboard. But the login behavior was already tested in the previous feature. You can quickly end up repeating those same steps in every scenario that needs an authenticated user.
I wanted a reusable step instead of repeating the login process everywhere. After some searching, I found that KNP Labs had run into the same problem. Their solution grouped the login process into one step with so-called meta-steps. That reduces the number of lines in the feature, but the browser still goes through the login process every time.
There had to be a better option. The Symfony cookbook explains how authentication can be simulated through tokens, and after some trial and error with Behat and Mink, I ended up with this code in my FeatureContext:
php<?phpuse Behat\Mink\Driver\BrowserKitDriver;use Symfony\Component\BrowserKit\Cookie;use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;/*** @Given /^I am authenticated as "([^"]*)"$/*/public function iAmAuthenticatedAs($username){$driver = $this->getSession()->getDriver();if (!$driver instanceof BrowserKitDriver) {throw new UnsupportedDriverActionException('This step is only supported by the BrowserKitDriver');}$client = $driver->getClient();$client->getCookieJar()->set(new Cookie(session_name(), true));$session = $client->getContainer()->get('session');$user = $this->kernel->getContainer()->get('fos_user.user_manager')->findUserByUsername($username);$providerKey = $this->kernel->getContainer()->getParameter('fos_user.firewall_name');$token = new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());$session->set('_security_'.$providerKey, serialize($token));$session->save();$cookie = new Cookie($session->getName(), $session->getId());$client->getCookieJar()->set($cookie);}
Now a scenario that needs an authenticated user can start like this:
gherkinScenario: Displaying the blog overviewGiven I am authenticated as "bar"And I am on "/admin/"Then I should see "Admin dashboard"
That is much better than writing the login process over and over again.