23 Jan 2015

Handle authenticated users when using Behat and Mink with Laravel 4

Categories: Laravel, Mink, Behat, Session

Behat is a great tool for testing your Laravel application. At Label305 we use Behat and Mink extensively to test several applications. But when using Mink drivers like Selenium to test your application it can take ages for all tests to finish. With the simple trick described below you can speedup your tests by creating a logged in state before the browser/client is fired up.

Preparing Application State

When executing behat it is smart to create application state when evaluating the Given ... statements without using the browser. So for example by filling the database. This way you prevent testing stuff in the browser twice.

When we just started out with Behat we created application state for our new scenarios by calling parts of previously implemented scenarios. So if the scenario required a registered user, the test would actualy go to the registration form, fill it out and register the user. This can add up when you have 100+ scenario’s and most of them require a registered user.

  Scenario: Some scenario
    Given there is a user with email "foo@bar.com"
    And I am logged in as "foo@bar.com"
    When I do something
    Then I should see something
    But I should not see something else

Database State

Implementing a step like Given there is a user with email "foo@bar.com" by populating the database is not that difficult of course. Just execute a couple of database queries.

<?php
class FeatureContext extends MinkContext {
    ...
    /**
     * @Given /^there is a user with email "([^"]*)"$/
     */
    public function thereIsAUserWithEmail($email)
    {
        User::where('email', $email)->forceDelete();

        User::create([
            'email' => $email,
            'password' => Hash::make('password'),
        ]);
    }
    ...
}

When using Eloquent models to query the database you need to have a booted application container. You can make sure this has happened by adding the following method to your FeatureContext.

<?php
class FeatureContext extends MinkContext {

    /**
     * @static
     * @beforeSuite
     */
    public static function bootstrapLaravel()
    {
        putenv('APP_ENV=behat'); // So this application has its own environment
        $app = require_once __DIR__ . '/../../../bootstrap/start.php';
        $app->boot();
    }

    ...
}

It is important that the application running from behat is using the right environment. Make sure detectEnvironment() is setup to give the application a seperate environment. I’ve used putenv() and getenv() in this example.

Session State

Now we will look at the step Given I am logged in as "foo@bar.com". When you are using Behat without an external server this is as simple as Auth::login($user);. You could use this approch when using the Behat Laravel Extension. The major disadvantage is that you can’t use JavaScript.

In our case, the Behat process is seperated from the server because we wanted to use Selenium. However, we want the user to be logged in without having to fill out the login for every scenario (which takes time, especialy when this is done 100+ times). How to accomplish this? By using something that looks like session hijacking, but without the hacking part.

Authentication state is stored to the session and a user specifies which session he/she is using by providing a cookie. We wanted to create a session in the Behat process and let Selenium use that session to let it execute its steps. So we gave Selenium a cookie, omnomnom.

<?php
class FeatureContext extends MinkContext {
    ...

    /**
     * @Given /^I am logged in as "([^"]*)"$/
     */
    public function iAmLoggedInAs($email)
    {
        // Destroy the previous session
        if (Session::isStarted()) {
            Session::regenerate(true);
        } else {
            Session::start();
        }

        // Login the user and since the driver and this code now
        // share a session this will also login the driver session
        $user = User::where('email', $email)->firstOrFail();
        Auth::login($user);

        // Save the session data to disk or to memcache
        Session::save();

        // Hack for Selenium
        // Before setting a cookie the browser needs to be launched
        if ($this->getSession()->getDriver() instanceof \Behat\Mink\Driver\Selenium2Driver) {
            $this->visit('login');
        }

        // Get the session identifier for the cookie
        $encryptedSessionId = Crypt::encrypt(Session::getId());
        $cookieName = Session::getName();

        // Set the cookie
        $minkSession = $this->getSession();
        $minkSession->setCookie($cookieName, $encryptedSessionId);
    }

    ...
}

This does not work out of the box, however. You will have to make sure that session data can be shared between the behat and server process. So you can’t use the array session provider. Using the file provider should be fine, and make sure both the environment of the server and Behat are using that setting.

Now for the tricky part. The Illuminate\Session\SessionServiceProvider has a method setupDefaultDriver() which makes sure the application always uses the array session provider when executing the application from the command line. So we need to extend the Illuminate\Session\SessionServiceProvider and create our own service provider and swap them out in the config/app.php. But in order to keep things clean, make sure this only happens in the behat environment (setup above in the bootstrapLaravel() method).

<?php namespace YourApp\Providers;

use Illuminate\Session\SessionServiceProvider;

class BehatSessionServiceProvider extends SessionServiceProvider {

    protected function setupDefaultDriver()
    {
        // Do nothing
        // Allows command line execution to save sessions
    }

}

So now your all set up to start using Selenium, Goutte or the other decoupled Mink drivers and keep your tests running relatively fast.

Written by: Thijs Scheepers