Testing Laravel Controllers

There are a two main things that tripped me up while I was writing functional tests for my Laravel controllers: POST requests, and session state.

Laravel’s Controller class has the call() method, which essentially makes a GET request to a controller method. In order to make POST requests, it’s necessary to inject some extra parameters into the HttpFoundation components. To make this easier, I created a ControllerTestCase class with convenient get() and post() methods:

abstract class ControllerTestCase extends PHPUnit_Framework_TestCase
{

    public function call($destination, $parameters = array(), $method = 'GET')
    {
        $old_method = Request::foundation()->getMethod();
        \Laravel\Request::foundation()->setMethod($method);
        $response = Controller::call($destination, $parameters);
        Request::foundation()->setMethod($old_method);

        return $response;
    }

    public function get($destination, $parameters = array())
    {
        return $this->call($destination, $parameters, 'GET');
    }

    public function post($destination, $post_data, $parameters = array())
    {
        $this->clean_request();
        \Laravel\Request::foundation()->request->add($post_data);

        return $this->call($destination, $parameters, 'POST');
    }

    private function clean_request()
    {
        $request = \Laravel\Request::foundation()->request;

        foreach ($request->keys() as $key)
        {
            $request->remove($key);
        }
    }

}

Note that each POST request must be “cleaned” so that the POST data from previous requests isn’t retained (thanks wahyudinata for this tip!).

This makes it easy to write functional tests for Laravel controllers, for example checking the session for errors after a POST request:

require_once('ControllerTestCase.php');

class AccountControllerTest extends ControllerTestCase
{

    public function testSignupWithNoData()
    {
        $response = $this->post('account@signup', array());
        $this->assertEquals('302', $response->foundation->getStatusCode());

        $session_errors = \Laravel\Session::instance()->get('errors')->all();
        $this->assertNotEmpty($session_errors);
    }

    public function testSignupWithValidData()
    {
        $response = $this->post('account@signup', array(
            'username' => 'validusername',
            'email' => 'some@validemail.com',
            'password' => 'passw0rd',
            'password_confirm' => 'passw0rd',
        ));
        $this->assertEquals('302', $response->foundation->getStatusCode());

        $session_errors = \Laravel\Session::instance()->get('errors');
        $this->assertNull($session_errors);
    }

}

But here is where the session state tripped me up. In testSignupWithValidData, the Laravel session state from testSignupWithNoData is retained and the test fails. To get around this, I simply reload the session before each test, in a setUp method in ControllerTestCase:

abstract class ControllerTestCase extends PHPUnit_Framework_TestCase
{

    // ...

    public function setUp()
    {
        \Laravel\Session::load();
    }

}

And that’s it! A fairly simple ControllerTestCase class which solves the POST request and session state problems. See this Gist for the full code with comments.

  • carbontwelve

    I have spent literally hours looking for a good starting point with using unittest on L3; I was all about ready to give up before stumbling upon your blog post and now have been able to complete writing several tests. Thank you.

  • Vladimir Kapustin

    Nice idea!

    But it broke my old tests for controllers. Here is my fix:

    public function call ($destination, $parameters = array(), $method = ‘GET’)
    {
    $old_method = Request::foundation()->getMethod();
    Request::foundation()->setMethod($method);
    $response = Controller::call($destination, $parameters);
    Request::foundation()->setMethod($old_method);

    return $response;
    }

    • wildlyinaccurate

      Thanks Vladimir! I’ll update the post to include this.

  • http://www.facebook.com/aykutfarsak Aykut Farsak

    This part:

    LaravelRequest::foundation()->server->add(array(
    ‘REQUEST_METHOD’ => $method,
    ));

    must be:

    LaravelRequest::foundation()->setMethod($method);

    • http://twitter.com/Joseph_Wynn Joseph Wynn

      Thanks Aykut, I’ve updated the snippet to use this method.

  • Ian

    Hi, not sure if you got the last reply because it errored out when submitting.

    Is it possible for you to share the actual account controller signup method to better understand the reasoning for the type of tests your running?

    I can not for the life of me figure out how to get an error into the session so the assertNotEmpty($session_errors) comes back with something. I’ve tried redirect with_error(), Session::flash(), Session::put(), Response, etc. and it always comes back null. Now under normal non testing circumstances I can flash errors and retrieve them in a view so I’m confused by the session instance coming back null.

    Thanks

    • http://wildlyinaccurate.com/ Joseph

      Hi Ian,

      Sorry, the WordPress comments haven’t been very reliable lately. Your other reply did go through, though.

      The code for the controller that I’m testing is here: https://github.com/wildlyinaccurate/magicrainbowadventure/blob/master/application/controllers/account.php#L197

      Line 220 is where the 302 happens: return Redirect::to('account/signup')->with_input()->with_errors($validation);

      If you’re doing the same and getting different results, could you please post your setup (OS, PHP version, etc) and also your controller code?

      • Ian

        Joseph, Thank you for your reply. I was trying to get Input::get(‘snippet’) which didn’t work for the ‘nodata’ test because it is sending an empty array. Using Input::all() fixed that.

        Then I was sending a custom error message if validation->fails() which is not an object so it caused the get(‘errors’)->all() to throw an exception. Fixed that by either sending the validation object or by removing the all() method from that line so it gets the ‘error’ I sent through.

        As for the 302, I was just confused because I was thinking I want to get a 200 status if it passes but forgot about the fact that a redirect is by default a 302 status.

        So basically I just need to learn more about proper coding in Laravel :-)

        Once I got those things sorted out your test works perfectly.

        Here’s my controller method

        public function post_create()
        {
        $input = Input::all();
        $rules = array(‘snippet’ => ‘required’);
        $validation = Validator::make($input, $rules);

        if($validation->fails()){
        return Redirect::to_route(‘snippets’)->with(‘errors’, ‘can not create snippet’);
        }

        return Redirect::to_route(‘snippet’, array(‘snippet’ => Input::get(‘snippet’)));

        }

  • Ian

    Thank you very much for this. It gives me a real good start.
    I do have a couple of issues though.
    When I use this same code but for a post_create() method I get ’200′ status for both the with data and no data tests. So I’m a little confused as to why you are testing to get a 302 status.

    This in turn causes $session_errors to be null in both cases so the first test for assertNotEmpty($session_errors) fails.

    Is this because in your actual account@signup you are redirecting with a 302 status?

    Sorry I’m easily confused sometimes :-)

  • http://gravatar.com/wahyudinata wahyudinata

    I just emailed you about a problem that I was having regarding previous request data getting merged with the new ones, I have found the error and this is my version (added clean_request and 2 more REST actions):

    http://pastebin.com/yXqxBemM

    • http://wildlyinaccurate.com/ Joseph

      Hey thanks a lot for finding this! I’ve updated the post to include your changes.

  • http://gravatar.com/wahyudinata wahyudinata

    Tried several times to “post” to my controller to no avail, and then I found this post. Thank you.

    PS: I feel this should really be rolled in to master.

    • http://wildlyinaccurate.com/ Joseph

      Glad you found it useful! Maybe I’ll submit a pull request to have it included in the core.

  • TazTahTouR

    Hey, nice article.
    Thanks for sharing your experience.

    But also, i have a question.
    Is there a way to run tests directly from the IDE (Netbeans in my case)?

    If i run tests from Netbeans, i get fatal error: class ‘Controller’ not found…

    • http://wildlyinaccurate.com/ Joseph

      Have you configured Netbeans to run the test task through artisan (php artisan test)? This will load a Laravel bootstrap to ensure the tests run correctly.

      • TazTahTouR

        I’d tried some settings in netbeans project properties, but i get some various errors. “Some” class not found or “some” file not found. Do you have an advice?

        Maybe i will try this later again, because deadline is near.

        Thank you, so far :-)

      • TazTahTouR

        Hello again,
        i’ve not found to run tests directly from netbeans but i’ve found a forum post to enable code coverage -> that was my main cause for running tests directly from netbeans.

        Here the link: http://forums.laravel.com/viewtopic.php?pid=8916#p8916