Testing controllers isn't the easiest thing in the world. Well, let me rephrase that: testing them is a cinch; what's difficult, at least at first, is determining what to test.
Should a controller test verify text on the page? Should it touch the database? Should it ensure that variables exist in the view? If this is your first hay-ride, these things can be confusing! Let me help.
Controller tests should verify responses, ensure that the correct database access methods are triggered, and assert that the appropriate instance variables are sent to the view.
The process of testing a controller can be divided into three pieces.
-
Isolate: Mock all dependencies (perhaps excluding the
View
). - Call: Trigger the desired controller method.
- Ensure: Perform assertions, verifying that the stage has been set properly.
The Hello World of Controller Testing
The best way to learn these things is through examples. Here's the "hello world" of controller testing in Laravel.
<?php # app/tests/controllers/PostsControllerTest.php class PostsControllerTest extends TestCase { public function testIndex() { $this->client->request('GET', 'posts'); } }
Laravel leverages a handful of Symfony's components to ease the process of testing routes and views, including HttpKernel, DomCrawler, and BrowserKit. This is why it's paramount that your PHPUnit tests inherit from, not PHPUnit\_Framework\_TestCase
, but TestCase
. Don't worry, Laravel still extends the former, but it helps setup the Laravel app for testing, as well as provides a variety of helper assertion methods that you are encouraged to use. More on that shortly.
In the code snippet above, we make a GET
request to /posts
, or localhost:8000/posts
. Assuming that this line is added to a fresh installation of Laravel, Symfony will throw a NotFoundHttpException
. If working along, try it out by running phpunit
from the command line.
$ phpunit 1) PostsControllerTest::testIndex Symfony\Component\HttpKernel\Exception\NotFoundHttpException:
In human-speak, this essentially translates to, "Hey, I tried to call that route, but you don't have anything registered, fool!"
As you can imagine, this type of request is common enough to the point that it makes sense to provide a helper method, such as $this->call()
. In fact, Laravel does that very thing! This means that the previous example can be refactored, like so:
#app/tests/controllers/PostsControllerTest.php public function testIndex() { $this->call('GET', 'posts'); }
Overloading is Your Friend
Though we'll stick with the base functionality in this chapter, in my personal projects, I take things a step further by allowing for such methods as $this->get()
, $this->post()
, etc. Thanks to PHP overloading, this only requires the addition of a single method, which you could add to app/tests/TestCase.php
.
# app/tests/TestCase.php public function __call($method, $args) { if (in_array($method, ['get', 'post', 'put', 'patch', 'delete'])) { return $this->call($method, $args[0]); } throw new BadMethodCallException; }
Now, you're free to write $this->get('posts')
and achieve the exact same result as the previous two examples. As noted above, however, let's stick with the framework's base functionality for simplicity's sake.
To make the test pass, we only need to prepare the proper route.
<?php # app/routes.php Route::get('posts', function() { return 'all posts'; });
Running phpunit
again will return us to green.
Laravel's Helper Assertions
A test that you'll find yourself writing repeatedly is one that ensures that a controller passes a particular variable to a view. For example, the index
method of PostsController
should pass a $posts
variable to its associated view, right? That way, the view can filter through all posts, and display them on the page. This is an important test to write!
If it's that common a task, then, once again, wouldn't it make sense for Laravel to provide a helper assertion to accomplish this very thing? Of course it would. And, of course, Laravel does!
Illuminate\Foundation\Testing\TestCase
includes a number of methods that will drastically reduce the amount of code needed to perform basic assertions. This list includes:
assertViewHas
assertResponseOk
assertRedirectedTo
assertRedirectedToRoute
assertRedirectedToAction
assertSessionHas
assertSessionHasErrors
The following examples calls GET /posts
and verifies that its views receives the variable, $posts
.
# app/tests/controllers/PostsControllerTest.php public function testIndex() { $this->call('GET', 'posts'); $this->assertViewHas('posts'); }
Tip: When it comes to formatting, I prefer to provide a line break between a test's assertion and the code that prepares the stage.
assertViewHas
is simply a bit of sugar that inspects the response object - which is returned from $this->call()
- and verifies that the data associated with the view contains a posts
variable.
When inspecting the response object, you have two core choices.
-
$response->getOriginalContent()
: Fetch the original content, or the returnedView
. Optionally, you may access theoriginal
property directly, rather than calling thegetOriginalContent
method. -
$response->getContent()
: Fetch the rendered output. If aView
instance is returned from the route, thengetContent()
will be equal to the HTML output. This can be helpful for DOM verifications, such as "the view must contain this string."
Let's assume that the posts
route consists of:
<?php # app/routes.php Route::get('posts', function() { return View::make('posts.index'); });
Should we run phpunit
, it will squawk with a helpful next step message:
1) PostsControllerTest::testIndex Failed asserting that an array has the key 'posts'.
To make it green, we simply fetch the posts and pass it to the view.
# app/routes.php Route::get('posts', function() { $posts = Post::all(); return View::make('posts.index', ['posts', $posts]); });
One thing to keep in mind is that, as the code currently stands, it only ensures that the variable, $posts
, is passed to the view. It doesn't inspect its value. The assertViewHas
optionally accepts a second argument to verify the value of the variable, as well as its existence.
# app/tests/controllers/PostsControllerTest.php public function testIndex() { $this->call('GET', 'posts'); $this->assertViewHas('posts', 'foo'); }
With this modified code, unles the view has a variable, $posts
, that is equal to foo
, the test will fail. In this situation, though, it's likely that we'd rather not specify a value, but instead declare that the value be an instance of Laravel's Illuminate\Database\Eloquent\Collection
class. How might we accomplish that? PHPUnit provides a helpful assertInstanceOf
assertion to fill this very need!
# app/tests/controllers/PostsControllerTest.php public function testIndex() { $response = $this->call('GET', 'posts'); $this->assertViewHas('posts'); // getData() returns all vars attached to the response. $posts = $response->original->getData()['posts']; $this->assertInstanceOf('Illuminate\Database\Eloquent\Collection', $posts); }
With this modification, we've declared that the controller must pass $posts
- an instance of Illuminate\Database\Eloquent\Collection
- to the view. Excellent.
Mocking the Database
There's one glaring problem with our tests so far. Did you catch it?
For each test, a SQL query is being executed on the database. Though this is useful for certain kinds of testing (acceptance, integration), for basic controller testing, it will only serve to decrease performance.
I've drilled this into your skull multiple times at this point. We're not interested in testing Eloquent's ability to fetch records from a database. It has its own tests. Taylor knows it works! Let's not waste time and processing power repeating those same tests.
Instead, it's best to mock the database, and merely verify that the appropriate methods are called with the correct arguments. Or, in other words, we want to ensure that Post::all()
never fires and hits the database. We know that works, so it doesn't require testing.
This section will depend heavily on the Mockery library. Please review that chapter from my book, if you're not yet familiar with it.
Required Refactoring
Unfortunately, so far, we've structured the code in a way that makes it virtually impossible to test.
# app/routes.php Route::get('posts', function() { // Ouch. We can't test this!! $posts = Post::all(); return View::make('posts.index') ->with('posts', $posts); });
This is precisely why it's considered bad practice to nest Eloquent calls into your controllers. Don't confuse Laravel's facades, which are testable and can be swapped out with mocks (Queue::shouldReceive()
), with your Eloquent models. The solution is to inject the database layer into the controller through the constructor. This requires some refactoring.
Warning: Storing logic within route callbacks is useful for small projects and APIs, but they make testing incredibly difficult. For applications of any considerable size, use controllers.
Let's register a new resource by replacing the posts
route with:
# app/routes.php Route::resource('posts', 'PostsController');
...and create the necessary resourceful controller with Artisan.
$ php artisan controller:make PostsController Controller created successfully!
Now, rather than referencing the Post
model directly, we'll inject it into the controller's constructor. Here's a condensed example that omits all restful methods except the one that we're currently interested in testing.
<?php # app/controllers/PostsController.php class PostsController extends BaseController { protected $post; public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->post->all(); return View::make('posts.index') ->with('posts', $posts); } }
Please note that it's a better idea to typehint an interface, rather than reference the Eloquent model, itself. But, one thing at a time! Let's work up to that.
This is a significantly better way to structure the code. Because the model is now injected, we have the ability to swap it out with a mocked version for testing. Here's an example of doing just that:
<?php # app/tests/controllers/PostsControllerTest.php class PostsControllerTest extends TestCase { public function __construct() { // We have no interest in testing Eloquent $this->mock = Mockery::mock('Eloquent', 'Post'); } public function tearDown() { Mockery::close(); } public function testIndex() { $this->mock ->shouldReceive('all') ->once() ->andReturn('foo'); $this->app->instance('Post', $this->mock); $this->call('GET', 'posts'); $this->assertViewHas('posts'); } }
The key benfit to this restructuring is that, now, the database will never needlessly be hit. Instead, using Mockery, we merely verify that the all
method is triggered on the model.
$this->mock ->shouldReceive('all') ->once();
Unfortunately, if you choose to forego coding to an interface, and instead inject the Post
model into the controller, a bit of trickery has to be used in order to get around Eloquent's use of statics, which can clash with Mockery. This is why we hijack both the Post
and Eloquent
classes within the test's constructor, before the official versions have been loaded. This way, we have a clean slate to declare any expectations. The downside, of course, is that we can't default to any existing methods, through the use of Mockery methods, like makePartial()
.
The IoC Container
Laravel's IoC container drastically eases the process of injecting dependencies into your classes. Each time a controller is requested, it is resolved out of the IoC container. As such, when we need to declare that a mock version of Post
should be used for testing, we only need to provide Laravel with the instance of Post
that should be used.
$this->app->instance('Post', $this->mock);
Think of this code as saying, "Hey Laravel, when you need an instance of
Post
, I want you to use my mocked version." Because the app extends theContainer
, we have access to all IoC methods directly off of it.
Upon instantiation of the controller, Laravel leverages the power of PHP reflection to read the typehint and inject the dependency for you. That's right; you don't have to write a single binding to allow for this; it's automated!
Redirections
Another common expectation that you'll find yourself writing is one that ensures that the user is redirected to the proper location, perhaps upon adding a new post to the database. How might we accomplish this?
# app/tests/controllers/PostsControllerTest.php public function testStore() { $this->mock ->shouldReceive('create') ->once(); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts'); $this->assertRedirectedToRoute('posts.index'); }
Assuming that we're following a restful flavor, to add a new post, we'd POST
to the collection, or posts
(don't confuse the POST
request method with the resource name, which just happens to have the same name).
$this->call('POST', 'posts');
Then, we only need to leverage another of Laravel's helper assertions, assertRedirectedToRoute
.
Tip: When a resource is registered with Laravel (
Route::resource()
), the framework will automatically register the necessary named routes. Runphp artisan routes
if you ever forget what these names are.
You might prefer to also ensure that the $_POST
superglobal is passed to the create
method. Even though we aren't physically submitting a form, we can still allow for this, via the Input::replace()
method, which allows us to "stub" this array. Here's the modified test, which uses Mockery's with()
method to verify the arguments passed to the method referenced by shouldReceive
.
# app/tests/controllers/PostsControllerTest.php public function testStore() { Input::replace($input = ['title' => 'My Title']);</p> $this->mock ->shouldReceive('create') ->once() ->with($input); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts'); $this->assertRedirectedToRoute('posts.index'); }
Paths
One thing that we haven't considered in this test is validation. There should be two separate paths through the store
method, dependent upon whether the validation passes:
- Redirect back to the "Create Post" form, and display the form validation errors.
- Redirect to the collection, or the named route,
posts.index
.
As a best practice, each test should represent but one path through your code.
This first path will be for failed validation.
# app/tests/controllers/PostsControllerTest.php public function testStoreFails() { // Set stage for a failed validation Input::replace(['title' => '']); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts'); // Failed validation should reload the create form $this->assertRedirectedToRoute('posts.create'); // The errors should be sent to the view $this->assertSessionHasErrors(['title']); }
The code snippet above explicitly declares which errors should exist. Alternatively, you may omit the argument to assertSessionHasErrors
, in which case it will merely verify that a message bag has been flashed (in translation, your Redirection includes withErrors($errors)
).
Now for the test that handles successful validation.
# app/tests/controllers/PostsControllerTest.php public function testStoreSuccess() { // Set stage for successful validation Input::replace(['title' => 'Foo Title']);</p> $this->mock ->shouldReceive('create') ->once(); $this->app->instance('Post', $this->mock); $this->call('POST', 'posts'); // Should redirect to collection, with a success flash message $this->assertRedirectedToRoute('posts.index', ['flash']); }
The production code for these two tests might look like:
# app/controllers/PostsController.php public function store() { $input = Input::all(); // We'll run validation in the controller for convenience // You should export this to the model, or a service $v = Validator::make($input, ['title' => 'required']); if ($v->fails()) { return Redirect::route('posts.create') ->withInput() ->withErrors($v->messages()); } $this->post->create($input); return Redirect::route('posts.index') ->with('flash', 'Your post has been created!'); }
Notice how the Validator
is nested directly in the controller? Generally, I'd recommend that you abstract this away to a service. That way, you can test your validation in isolation from any controllers or routes. Nonetheless, let's leave things as they are for simplicity's sake. One thing to keep in mind is that we aren't mocking the Validator
, though you certainly could do so. Because this class is a facade, it can easily be swapped out with a mocked version, via the Facade's shouldReceive
method, without us needing to worry about injecting an instance through the constructor. Win!
# app/controllers/PostsController.php Validator::shouldReceive('make') ->once() ->andReturn(Mockery::mock(['fails' => 'true']));
From time to time, you'll find that a method that needs to be mocked should return an object, itself. Luckily, with Mockery, this is a piece of cake: we only need to create an anonymous mock, and pass an array, which signals the method name and response value, respectively. As such:
Mockery::mock(['fails' => 'true'])
will prepare an object, containing a fails()
method that returns true
.
Repositories
To allow for optimal flexibility, rather than creating a direct link between your controller and an ORM, like Eloquent, it's better to code to an interface. The considerable advantage to this approach is that, should you perhaps need to swap out Eloquent for, say, Mongo or Redis, doing so literally requires the modification of a single line. Even better, the controller doesn't ever need to be touched.
Repositories represent the data access layer of your application.
What might an interface for managing the database layer of a Post
look like? This should get you started.
<?php # app/repositories/PostRepositoryInterface.php interface PostRepositoryInterface { public function all(); public function find($id); public function create($input); }
This can certainly be extended, but we've added the bare minimum methods for the demo: all
, find
, and create
. Notice that the repository interfaces are being stored within app/repositories
. Because this folder is not autoloaded by default, we need to update the composer.json
file for the application to reference it.
// composer.json "autoload": { "classmap": [ // .... "app/repositories" ] }
When a new class is added to this directory, don't forget to
composer dump-autoload -o
. The-o
, (optimize) flag is optional, but should always be used, as a best practice.
If you attempt to inject this interface into your controller, Laravel will snap at you. Go ahead; try it out and see. Here's the modified PostController
, which has been updated to inject an interface, rather than the Post
Eloquent model.
<?php # app/controllers/PostsController.php use Repositories\PostRepositoryInterface as Post; class PostsController extends BaseController { protected $post; public function __construct(Post $post) { $this->post = $post; } public function index() { $posts = $this->post->all(); return View::make('posts.index', ['posts' => $posts]); } }
If you run the server and view the output, you'll be met with the dreaded (but beautiful) Whoops error page, declaring that "PostRepositoryInterface is not instantiable."
If you think about it, of course the framework is squawking! Laravel is smart, but it's not a mind reader. It needs to be told which implementation of the interface should be used within the controller.
For now, let's add this binding to app/routes.php
. Later, we'll instead make use of service providers to store this sort of logic.
# app/routes.php App::bind( 'Repositories\PostRepositoryInterface', 'Repositories\EloquentPostRepository' );
Verbalize this function call as, "Laravel, baby, when you need an instance of PostRepositoryInterface
, I want you to use EloquentPostRepository
."
app/repositories/EloquentPostRepository
will simply be a wrapper around Eloquent that implements PostRepositoryInterface
. This way, we're not restricting the API (and every other implementation) to Eloquent's interpretation; we can name the methods however we wish.
<?php namespace Repositories; # app/repositories/EloquentPostRepository.php use Repositories\PostRepositoryInterface; use Post; class EloquentPostRepository implements PostRepositoryInterface { public function all() { return Post::all(); } public function find($id) { return Post::find($id); } public function create($input) { return Post::create($input); } }
Some might argue that the
Post
model should be injected into this implementation for testability purposes. If you agree, simply inject it through the constructor, per usual.
That's all it should take! Refresh the browser, and things should be back to normal. Only, now, your application is far better structured, and the controller is no longer linked to Eloquent.
Let's imagine that, a few months from now, your boss informs you that you need to swap Eloquent out with Redis. Well, because you've structured your application in this future-proof way, you only need to create the new app/repositories/RedisPostRepository
implementation:
<?php namespace Repositories; # app/repositories/RedisPostRepository.php use Repositories\PostRepositoryInterface; class RedisPostRepository implements PostRepositoryInterface { public function all() { // return all with Redis } public function find($id) { // return find one with Redis } public function create($input) { // return create with Redis } }
And update the binding:
# app/routes.php App::bind( 'Repositories\PostRepositoryInterface', 'Repositories\RedisPostRepository' );
Instantly, you're now leveraging Redis in your controller. Notice how app/controllers/PostsController.php
was never touched? That's the beauty of it!
Structure
So far in this lesson, our organization has been a bit lacking. IoC bindings in the routes.php
file? All repositories grouped together in one directory? Sure, that may work in the beginning, but, very quickly, it'll become apparent that this doesn't scale.
In the final section of this article, we'll PSR-ify our code, and leverage service providers to register any applicable bindings.
PSR-0 defines the mandatory requirements that must be adhered to for autoloader interoperability.
A PSR-0 loader may be registered with Composer, via the psr-0
object.
// composer.json "autoload": { "psr-0": { "Way": "app/lib/" } }
The syntax can be confusing at first. It certainly was for me. An easy way to decipher "Way": "app/lib/"
is to think to yourself, "The base folder for the Way
namespace is located in app/lib
." Of course, replace my last name with the name of your project. The directory structure to match this would be:
- app/
- lib/
- Way/
Next, rather than grouping all repositories into a repositories
directory, a more elegant approach might be to categorize them into multiple directories, like so:
- app/
- lib/
- Way/
- Storage/
- Post/
- PostRepositoryInterface.php
- EloquentPostRepository.php
It's vital that we adhere to this naming and folder convention, if we want the autoloading to work as expected. The only remaining thing to do is update the namespaces for PostRepositoryInterface
and EloquentPostRepository
.
<?php namespace Way\Storage\Post; # app/lib/Way/Storage/Post/PostRepositoryInterface.php interface PostRepositoryInterface { public function all(); public function find($id); public function create($input); }
And for the implementation:
<?php namespace Way\Storage\Post; # app/lib/Way/Storage/Post/EloquentPostRepository.php use Post; class EloquentPostRepository implements PostRepositoryInterface { public function all() { return Post::all(); } public function find($id) { return Post::find($id); } public function create($input) { return Post::create($input); } }
There we go; that's much cleaner. But what about those pesky bindings? The routes file may be a convenient place to experiment, but it makes little sense to store them there permanently. Instead, we'll use service providers.
Service providers are nothing more than bootstrap classes that can be used to do anything you wish: register a binding, hook into an event, import a routes file, etc.
A service provider's register()
will be triggered automatically by Laravel.
<?php namespace Way\Storage; # app/lib/Way/Storage/StorageServiceProvider.php use Illuminate\Support\ServiceProvider; class StorageServiceProvider extends ServiceProvider { // Triggered automatically by Laravel public function register() { $this->app->bind( 'Way\Storage\Post\PostRepositoryInterface', 'Way\Storage\Post\EloquentPostRepository' ); } }
To make this file known to Laravel, you only need to include it in app/config/app.php
, within the providers
array.
# app/config/app.php 'providers' => array( 'Illuminate\Foundation\Providers\ArtisanServiceProvider', 'Illuminate\Auth\AuthServiceProvider', // ... 'Way\Storage\StorageServiceProvider' )
Good; now we have a dedicated file for registering new bindings.
Updating the Tests
With our new structure in place, rather than mocking the Eloquent model, itself, we can instead mock PostRepositoryInterface
. Here's an example of one such test:
# app/tests/controllers/PostsControllerTest.php public function testIndex() { $mock = Mockery::mock('Way\Storage\Post\PostRepositoryInterface'); $mock->shouldReceive('all')->once(); $this->app->instance('Way\Storage\Post\PostRepositoryInterface', $mock); $this->call('GET', 'posts'); $this->assertViewHas('posts'); }
However, we can improve this. It stands to reason that every method within PostsControllerTest
will require a mocked version of the repository. As such, it's better to extract some of this prep work into its own method, like so:
# app/tests/controllers/PostsControllerTest.php public function setUp() { parent::setUp(); $this->mock('Way\Storage\Post\PostRepositoryInterface'); } public function mock($class) { $mock = Mockery::mock($class); $this->app->instance($class, $mock); return $mock; } public function testIndex() { $this->mock->shouldReceive('all')->once(); $this->call('GET', 'posts'); $this->assertViewHas('posts'); }
Not bad, ay?
Now, if you want to be super-fly, and are willing to add a touch of test logic to your production code, you could even perform your mocking within the Eloquent model! This would allow for:
Post::shouldReceive('all')->once();
Behind the scenes, this would mock PostRepositoryInterface
, and update the IoC binding. You can't get much more readable than that!
Allowing for this syntax only requires you to update the Post
model, or, better, a BaseModel
that all of the Eloquent models extend. Here's an example of the former:
<?php # app/models/Post.php class Post extends Eloquent { public static function shouldReceive() { $class = get_called_class(); $repo = "Way\\Storage\\{$class}\\{$class}RepositoryInterface"; $mock = Mockery::mock($repo); App::instance($repo, $mock); return call_user_func_array([$mock, 'shouldReceive'], func_get_args()); } }
If you can manage the inner "Should I be embedding test logic into production code" battle, you'll find that this allows for significantly more readable tests.
<?php # app/tests/controllers/PostsControllerTest.php class PostsControllerTest extends TestCase { public function tearDown() { Mockery::close(); } public function testIndex() { Post::shouldReceive('all')->once(); $this->call('GET', 'posts'); $this->assertViewHas('posts'); } public function testStoreFails() { Input::replace($input = ['title' => '']); $this->call('POST', 'posts'); $this->assertRedirectedToRoute('posts.create'); $this->assertSessionHasErrors(); } public function testStoreSuccess() { Input::replace($input = ['title' => 'Foo Title']); Post::shouldReceive('create')->once(); $this->call('POST', 'posts'); $this->assertRedirectedToRoute('posts.index', ['flash']); } }
It feels good, doesn't it? Hopefully, this article hasn't been too overwhelming. The key is to learn how to organize your repositories in such a way to make them as easy as possible to mock and inject into your controllers. As a result of that effort, your tests will be lightning fast!
This article is an excerpt from my upcoming book, Laravel Testing Decoded. Stay tuned for its release in May, 2013!
Comments