Testing Laravel Controllers

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.

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.

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:

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.

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.

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.

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 returned View. Optionally, you may access the original property directly, rather than calling the getOriginalContent method.
  • $response->getContent(): Fetch the rendered output. If a View instance is returned from the route, then getContent() 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:

Should we run phpunit, it will squawk with a helpful next step message:

To make it green, we simply fetch the posts and pass it to the view.

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.

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!

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.

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:

...and create the necessary resourceful controller with Artisan.

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.

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:

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.

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.

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 the Container, 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?

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).

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. Run php 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.


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:

  1. Redirect back to the "Create Post" form, and display the form validation errors.
  2. 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.

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.

The production code for these two tests might look like:

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!

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:

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.

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.

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.

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."

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.

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.

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:

And update the binding:

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.

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.

And for the implementation:

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.

To make this file known to Laravel, you only need to include it in app/config/app.php, within the providers array.

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:

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:

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:

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:

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.

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!

Tags:

Comments

Related Articles