Robin van der Vleuten

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:

gherkin
Feature: Sign in to the website
In order to access the administrative interface
As a visitor
I need to be able to log in to the website
Background:
Given there are following users:
| username | email | plain_password | enabled |
| bar | [email protected] | foo | yes |
Scenario: Log in with username and password
Given 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 credentials
Given 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:

gherkin
Feature: Use the admin dashboard
In order to see the current status of my application
As an administrative user
I need to be able to use the admin dashboard
Background:
Given there are following users:
| username | email | plain_password | enabled |
| bar | [email protected] | foo | yes |
Scenario: Displaying the blog overview
Given 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
<?php
use 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:

gherkin
Scenario: Displaying the blog overview
Given 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.