Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel 5 isolated controller testing

I'm trying to unit test my controllers in Laravel 5, but have serious issues wrapping my head around it. It seems as I have to trade the great short-hand functions and static classes for dependency injected equivalents if I actually want to do isolated unit testing.

First off, what I see in the documentation as "Unit testing", is not unit testing to me. It seems more like functional testing. I can not test a controller function isolated, as I will have to go through the entire framework, and will need to actually seed my database if I have any code interacting with my database.

So, in turn, I want to test my controllers isolated of the framework. This is however proving to be quite difficult.

Let's look at this example function (I've kept out some parts of this function for the sake of the question):

public function postLogin(\Illuminate\Http\Request $request)
{
    $this->validate($request, [
        'email' => 'required|email', 'password' => 'required',
    ]);

    $credentials = $request->only('email', 'password');

    if (Auth::attempt($credentials, $request->has('remember')))
    {
        return redirect()->intended($this->redirectPath());
    }
}

Now, the problem arises in the final lines. Sure, I can mock the Request instance that's sent to the function, that's no issue. But how am I going to mock the Auth class, or the redirect function? I need to rewrite my class/function with dependency injection like this:

private $auth;
private $redirector;

public function __construct(Guard $auth, \Illuminate\Routing\Redirector $redirector) 
{
    $this->auth = $auth;
    $this->redirector = $redirector;
}

public function postLogin(\Illuminate\Http\Request $request)
{
    $this->validate($request, [
        'email' => 'required|email', 'password' => 'required',
    ]);

    $credentials = $request->only('email', 'password');

    if ($this->auth->attempt($credentials, $request->has('remember')))
    {
        return $this->redirector->intended($this->redirectPath());
    }
}

And I end up with a convoluted unit test, full of mocks:

public function testPostLoginWithCorrectCredentials()
{
    $guardMock = \Mockery::mock('\Illuminate\Contracts\Auth\Guard', function($mock){
        $mock->shouldReceive('attempt')->with(['email' => 'test', 'password' => 'test'], false)->andReturn(true);
    });

    $redirectorMock = \Mockery::mock('\Illuminate\Routing\Redirector', function($mock){
        $mock->shouldReceive('intended')->andReturn('/somePath');
    });

    $requestMock = \Mockery::mock('\Illuminate\Http\Request', function($mock){
        $mock->shouldReceive('only')->with('email', 'password')->andReturn(['email' => 'test', 'password' => 'test']);
        $mock->shouldReceive('has')->with('remember')->andReturn(false);
    });

    $object = new AuthController($guardMock, $redirectorMock);
    $this->assertEquals('/somePath', $object->postLogin($requestMock));
}

Now, if I had any more complex logic that would, for example, use a model, I'd have to dependency inject that as well, and mock it in my class.

To me, it seems like either, Laravel isn't providing what I want it to do, or my testing logic is flawed. Is there any way I can test my controller functions without getting out-of-control testing functions and/or having to depedency inject standard Laravel classes, available to me anyways in my controller?

like image 258
Deniz Zoeteman Avatar asked Apr 05 '15 13:04

Deniz Zoeteman


1 Answers

You shouldnt be trying to unit test controllers. They are designed to be functional tested via calling them through the HTTP protocol. This is how controllers are designed, and Laravel provides a number of framework assertions you can include in your tests to ensure they work as expected.

But if you want to unit test the application code contained within the controller, then you should actually consider using Commands.

Using Commands allows you to extracte the application logic from your controller into a class. You can then unit test the class/command to ensure you get the results expected.

You can then simply call the command from the controller.

In fact the Laravel documentation tells you this:

We could put all of this logic inside a controller method; however, this has several disadvantages... it is more difficult to unit-test the command as we must also generate a stub HTTP request and make a full request to the application to test the purchase podcast logic.

like image 92
Laurence Avatar answered Nov 07 '22 16:11

Laurence