Unit testing your Laravel controllers

Published by at 25th February 2018 3:50 pm

In my previous post I mentioned some strategies for refactoring Laravel controllers to move unnecessary functionality elsewhere. However, I didn't cover testing them. In this post I will demonstrate the methodology I use for testing Laravel controllers.

Say we have the following method in a controller:

1public function store(Request $request)
2{
3 $document = new Document($request->only([
4 'title',
5 'text',
6 ]));
7 $document->save();
8
9 event(new DocumentCreated($document));
10
11 return redirect()->route('/');
12}

This controller method does three things:

  • Return a response
  • Create a model instance
  • Fire an event

Our tests therefore need to pass it all its external dependencies and check it carries out the required actions.

First we fake the event facade:

Event::fake();

Next, we create an instance of Illuminate\Http\Request to represent the HTTP request passed to the controller:

1 $request = Request::create('/store', 'POST',[
2 'title' => 'foo',
3 'text' => 'bar',
4 ]);

If you're using a custom form request class, you should instantiate that in exactly the same way.

Then, instantiate the controller, and call the method, passing it the request object:

1 $controller = new MyController();
2 $response = $controller->store($request);

You can then test the response from the controller. You can test the status code like this:

$this->assertEquals(302, $response->getStatusCode());

You may also need to check the content of the response matches what you expect to see, by retrieving $response->getBody()->getContent().

Next, retrieve the newly created model instance, and verify it exists:

1 $document = Document::where('title', 'foo')->first();
2 $this->assertNotNull($document);

You can also use assertEquals() to check the attributes on the model if appropriate. Finally, you check the event was fired:

1 Event::assertDispatched(DocumentCreated::class, function ($event) use ($document) {
2 return $event->document->id === $document->id;
3 });

This test should not concern itself with any functionality triggered by the event, only that the event gets triggered. The event should have separate unit tests in which the event is triggered, and then the test verifies it carried out the required actions.

Technically, these don't quite qualify as being unit tests because they hit the database, but they should cover the controller adequately. To make them true unit tests, you'd need to implement the repository pattern for the database queries rather than using Eloquent directly, and mock the repository, so you can assert that the mocked repository receive the right data and have it return the expected response.

Here is how you might do that with Mockery:

1$mock = Mockery::mock('App\Contracts\Repositories\Document');
2$mock->shouldReceive('create')->with([
3 'title' => 'foo',
4 'text' => 'bar',
5])->once()->andReturn(true);
6$controller = new MyController($mock);

As long as your controllers are kept as small as possible, it's generally not too hard to test them. Unfortunately, fat controllers become almost impossible to test, which is another good reason to avoid them.