Книга: Laravel 4 Cookbook
Назад: File-Based CMS
На главную: Предисловие

Controller Testing

Testing is a hot topic these days. Everyone knows that having tests is a good thing, but often they’re either “too busy” to write them, or they just don’t know how.

Even if you do write tests, it may not always be clear what the best way is to prepare for them. Perhaps you’re used to writing them against a particular framework (which I’m assuming is not Laravel 4) or you tend to write fewer when you can’t figure out how exactly to test a particular section of your application.

I have recently spent much time writing tests, and learning spades about how to write them. I have Jeffrey Way to thank, both for and . These resources have taught me just about everything I know about unit/functional testing.

This article is about some of the practical things you can do to make your code easier to test, and how you would go about writing functional/unit tests against that code. If you take nothing else away, after reading this, you should be subscribed to Laracasts.com and you should read Laravel: Testing Decoded.

Since there’s already so much in the way of testing, I thought it would be better just to focus on the subject of how to write testable controllers, and then how to test that they are doing what they are supposed to be doing.

The code for this chapter can be found at:

Installing Dependencies

For this chapter, we’re going to be using PHPUnit and Mockery. Both of these libraries are used in testing, and can be installed at the same time, by running the following commands:

 1 ❯ composer require --dev "phpunit/phpunit:4.0.*"  2   3 ./composer.json has been updated  4 Loading composer repositories with package information  5 ...  6   7 ❯ composer require --dev "mockery/mockery:0.9.*"  8   9 ./composer.json has been updated 10 Loading composer repositories with package information 11 ... 

We’ll get into the specifics of each later, but this should at least install them and add them to the require-dev section of your composer.json file.

Unit vs. Functional vs. Acceptance

There are many kinds of tests. There are three which we are going to look at:

  1. Unit
  2. Functional
  3. Acceptance

Unit Tests

Unit tests are tests which cover individual functions or methods. They are meant to run the functions or methods in complete isolation, with no dependencies or side-effects.

You can find a boring description at: .

Functional Tests

Functional tests are tests which concentrate on the input given to some function or method, and the output returned. They don’t care about isolation or state.

You can find a boring description at: .

Acceptance Tests

Acceptance tests are test which look at a much broader range of functionality. These are often called end-to-end tests because they are concerned with the correct functionality over a range of functions, methods classes etc.

You can find a boring description at: .

Am I Writing Unit Or Functional Tests?

When it comes to writing tests in Laravel 4, developers often think they are writing unit tests when they are actually writing functional tests. The difference is small but significant.

Laravel provides a set of test classes (a base TestCase class, and an ExampleTest class). If you base your tests off of these, you are probably writing functional tests. The TestCase class actually initialises the Laravel 4 framework. If your code depends on that being the case (accessing the aliases, service providers etc.) then your tests aren’t isolated. They have dependencies and they need to be run in order.

When your tests only require the underlying PHPUnit or PHPSpec classes, then you may be writing unit tests.

How does this affect us? Well - if your goal is to write functional tests, and you have decent test coverage then you’re doing ok. But if you want to write true unit tests, then you need to pay attention to how your code is constructed. If you’re using the handy aliases, which Laravel provides, then writing unit tests may be tough.

That should be enough theory to get us started. Let’s take a look at some code…

Fat Controllers

It’s not uncommon to find controllers with actions resembling the following:

 1 public function store()  2 {  3   $validator = Validator::make(Input::all(), [  4     "title"    => "required|max:50",  5     "subtitle" => "required|max:100",  6     "body"     => "required",  7     "author"   => "required|exists:authors"  8   ]);  9    10   if ($validator->passes()) { 11     Posts::create([ 12       "title"     => Input::get("title"), 13       "subtitle"  => Input::get("subtitle"), 14       "body"      => Input::get("body"), 15       "author_id" => Input::get("author"), 16       "slug"      => Str::slug(Input::get("title")) 17     ]); 18    19     Mail::send("emails.post", Input::all(), function($email) { 20       $email 21         ->to("[email protected]", "Chris") 22         ->subject("New post"); 23     }); 24    25     return Redirect::route("posts.index"); 26   } 27    28   return Redirect::back() 29     ->withErrors($validator) 30     ->withInput(); 31 } 

This was extracted from app/controllers/PostController.php. I generated the file with the controller:make command.

This is how we first learn to use MVC frameworks, but there comes a point where we are familiar with how they work, and need to start writing tests. Testing this action would be a nightmare. Fortunately, there are a few improvements we can make.

Service Providers

We’ve used service providers to organise and package our code. Now we’re going to use them to help us thin our controllers out. Create a new service provider, resembling the following:

 1 <?php  2     3 namespace Formativ;  4     5 use Illuminate\Support\ServiceProvider;  6     7 class PostServiceProvider  8 extends ServiceProvider  9 { 10   protected $defer = true; 11    12   public function register() 13   { 14     $this->app->bind( 15       "Formativ\\PostRepositoryInterface", 16       "Formativ\\PostRepository" 17     ); 18    19     $this->app->bind( 20       "Formativ\\PostValidatorInterface", 21       "Formativ\\PostValidator" 22     ); 23    24     $this->app->bind( 25       "Formativ\\PostMailerInterface", 26       "Formativ\\PostMailer" 27     ); 28   } 29    30   public function provides() 31   { 32     return [ 33       "Formativ\\PostRepositoryInterface", 34       "Formativ\\ValidatorInterface", 35       "Formativ\\MailerInterface" 36     ]; 37   } 38 } 

This file should be saved as app/Formativ/PostServiceProvider.php.

You will also need to load the Formativ namespace through the composer.json file. You can do that by using either PSR specification, or even through a classmap. More info on that at: .

You can find out more about making service providers at: .

This does a couple of important things:

  1. Interfaces are connected to concrete implementations, so that we can type-hint the interfaces in our controller, and they will be resolved automatically, with the Laravel IoC container.
  2. We specify which interfaces are provided (in the provides() method) so that we can defer the loading of this service provider until the concrete implementations are called.

We need to define the interfaces and concrete implementations:

 1 <?php  2     3 namespace Formativ;  4     5 interface PostRepositoryInterface  6 {  7   public function all(array $modifiers);  8   public function first(array $modifiers);  9   public function insert(array $data); 10   public function update(array $data, array $modifiers); 11   public function delete(array $modifiers); 12 } 

This file should be saved as app/Formativ/PostRepositoryInterface.php.

 1 <?php  2     3 namespace Formativ;  4     5 class PostRepository implements PostRepositoryInterface  6 {  7   public function all(array $modifiers)  8   {  9     // return all the posts filtered by $modifiers... 10   } 11    12   public function first(array $modifiers) 13   { 14     // return the first post filtered by $modifiers... 15   } 16    17   public function insert(array $data) 18   { 19     // insert posts with $data... 20   } 21    22   public function update(array $data, array $modifiers) 23   { 24     // update posts filtered by $modifiers, with $data... 25   } 26    27   public function delete(array $modifiers) 28   { 29     // delete posts filtered by $modifiers... 30   } 31 } 

This file should be saved as app/Formativ/PostRepository.php.

 1 <?php  2     3 namespace Formativ;  4     5 interface PostValidatorInterface  6 {  7   public function passes($event);  8   public function messages($event);  9   public function on($event); 10 } 

This file should be saved as app/Formativ/PostValidatorInterface.php.

 1 <?php  2     3 namespace Formativ;  4     5 class PostValidator implements PostValidatorInterface  6 {  7   public function passes($event)  8   {  9     // validate the event instance... 10   } 11    12   public function messages($event) 13   { 14     // fetch the error messages for the event instance... 15   } 16    17   public function on($event) 18   { 19     // set up the event instance and return it for method chaining... 20   } 21 } 

This file should be saved as app/Formativ/PostValidator.php.

1 <?php 2    3 namespace Formativ; 4    5 interface PostMailerInterface 6 { 7   public function send($to, $view, $data); 8 } 

This file should be saved as app/Formativ/PostMailerInterface.php.

 1 <?php  2     3 namespace Formativ;  4     5 class PostMailer implements PostMailerInterface  6 {  7   public function send($to, $view, $data)  8   {  9     // send an email about the post... 10   } 11 } 

This file should be saved as app/Formativ/PostMailer.php.

Dependency Injection

With all of these interfaces and concrete implementations in place, we can simply type-hint the interfaces in our controller. This is essentially dependency injection. We don’t create or use dependencies in our controller - rather they are passed in when the controller is instantiated.

The dependencies are resolved automatically, via the IoC container and reflection.

This makes the controller thinner, and helps us break the logic up into a number of smaller, easier-to-test classes:

 1 <?php  2     3 use Formativ\PostRepositoryInterface;  4 use Formativ\PostValidatorInterface;  5 use Formativ\PostMailerInterface;  6 use Illuminate\Support\Facades\Response;  7     8 class PostController  9 extends BaseController 10 { 11   public function __construct( 12     PostRepositoryInterface $repository, 13     PostValidatorInterface $validator, 14     PostMailerInterface $mailer, 15     Response $response 16   ) 17   { 18     $this->repository = $repository; 19     $this->validator  = $validator; 20     $this->mailer     = $mailer; 21     $this->response   = $response; 22   } 23    24   public function store() 25   { 26     if ($this->validator->passes("store"))) { 27       $this->repository->insert([ 28         "title"     => Input::get("title"), 29         "subtitle"  => Input::get("subtitle"), 30         "body"      => Input::get("body"), 31         "author_id" => Input::get("author"), 32         "slug"      => Str::slug(Input::get("title")) 33       ]); 34 			   35       $this->mailer->send("[email protected]", "emails.post"); 36    37       return $this->response 38         ->route("posts.index") 39         ->with("success", true); 40     } 41    42     return $this->response 43       ->back() 44       ->withErrors($this->validator->messages("store")) 45       ->withInput(); 46   } 

This was extracted from app/controllers/PostController.php.

Another thing you can do, to further modularise your logic, is to dispatch events at critical points in execution:

 1 <?php  2     3 use Formativ\PostRepositoryInterface;  4 use Formativ\PostValidatorInterface;  5 use Formativ\PostMailerInterface;  6 use Illuminate\Support\Facades\Response;  7 use Illuminate\Events\Dispatcher;  8     9 class PostController 10 extends BaseController 11 { 12   public function __construct( 13     PostRepositoryInterface $repository, 14     PostValidatorInterface $validator, 15     PostMailerInterface $mailer, 16     Response $response, 17     Dispatcher $dispatcher 18   ) 19   { 20     $this->repository = $repository; 21     $this->validator  = $validator; 22     $this->mailer     = $mailer; 23     $this->response   = $response; 24     $this->dispatcher = $dispatcher; 25    26     $this->dispatcher->listen( 27       "post.store", 28       [$this->repository, "insert"] 29     ); 30 		   31     $this->dispatcher->listen( 32       "post.store", 33       [$this->mailer, "send"] 34     ); 35   } 36    37   public function store() 38   { 39     if ($this->validator->passes("store")) { 40       $this->dispatcher->fire("post.store"); 41    42       return $this->response 43         ->route("posts.index") 44         ->with("success", true); 45     } 46    47     return $this->response 48       ->back() 49       ->withErrors($this->validator->messages("store")) 50       ->withInput(); 51   } 

This was extracted from app/controllers/PostController.php.

 1 <?php  2     3 namespace Formativ;  4     5 use Illuminate\Http\Request;  6 use Str;  7     8 class PostRepository implements PostRepositoryInterface  9 { 10   public function __construct(Request $request) 11   { 12     $this->request = $request; 13   } 14    15   public function insert() 16   { 17     $data = [ 18       "title"     => $this->request->get("title"), 19       "subtitle"  => $this->request->get("subtitle"), 20       "body"      => $this->request->get("body"), 21       "author_id" => $this->request->get("author"), 22       "slug"      => Str::slug($this->request->get("title")) 23     ]; 24        25     // insert posts with $data... 26   } 

This was extracted from app/Formativ/PostRepository.php.

Using this approach, you can delegate method calls based on events, rather than explicit method calls and variable manipulation.

There are loads of different ways to use events, so you should definitely check out the official docs at: .

This Isn’t Testing!

If you’re wondering how this is related to testing, consider how you would have tested the original controller code. There are many different responsibilities, and places for errors to emerge.

Splitting off your logic into classes of single responsibility is a good thing. Generally following SOLID principles is a good thing. The smaller your classes are, the fewer things each of them do, the easier they are to test.

So how would we write tests for these new classes? Let’s begin with one of the concrete implementations:

 1 <?php  2     3 namespace Formativ;  4     5 class PostMailerTest  6 extends TestCase  7 {  8   public function testSend()  9   { 10     // ...your test here 11   } 12 } 

This file should be saved as app/tests/Formativ/PostMailerTest.php.

In order for this first test case to run, we’ll need to set up a phpunit config file:

 1 <?xml version="1.0" encoding="UTF-8"?>  2 <phpunit backupGlobals="false"  3   backupStaticAttributes="false"  4   bootstrap="bootstrap/autoload.php"  5   colors="true"  6   convertErrorsToExceptions="true"  7   convertNoticesToExceptions="true"  8   convertWarningsToExceptions="true"  9   processIsolation="false" 10   stopOnFailure="false" 11   syntaxCheck="false" 12 > 13   <testsuites> 14     <testsuite name="Application Test Suite"> 15       <directory>./app/tests/</directory> 16     </testsuite> 17   </testsuites> 18 </phpunit> 

This file should be saved as phpunit.xml.

This will get the tests running, and serves as a template in which to start writing our tests. You can actually see this test case working, by running the following command:

 1 ❯ phpunit  2     3 PHPUnit 4.0.14 by Sebastian Bergmann.  4     5 Configuration read from /path/to/phpunit.xml  6     7 .  8     9 Time: 76 ms, Memory: 8.75Mb 10    11 OK (1 test, 0 assertions) 

This test is functional because it only happens after a Laravel instance is spun up, and it doesn’t care about what state it leaves this application instance in.

Since we haven’t implemented the body of the send() method, it’s difficult for us to know what the return value will be. What we can test for is what methods (on the mailer’s underlying mail transport/interface) are being called…

Imagine we’re using the underlying Laravel mail class to send the emails. We used it before we started optimising the controller layer:

1 Mail::send("emails.post", Input::all(), function($email) { 2   $email 3     ->to("[email protected]", "Chris") 4     ->subject("New post"); 5 }); 

You can find out more about the Mailer class at: .

We’d essentially like to use this logic inside the PostMailer class. We should also dependency-inject our Mail provider:

 1 <?php  2     3 namespace Formativ;  4     5 use Illuminate\Mail\Mailer;  6     7 class PostMailer implements PostMailerInterface  8 {  9   public function __construct(Mailer $mailer) 10   { 11     $this->mailer = $mailer; 12   } 13    14   public function send($to, $view, $data) 15   { 16     $this->mailer->send( 17       $view, $data, 18       function($email) use ($to) { 19         $email->to($to); 20       } 21     ); 22   } 23 } 

This file should be saved as app/Formativ/PostMailer.php.

Now we call the send() method on an injected mailer instance, instead of directly on the facade. This is still a little tricky to test (thanks to the callback), but thankfully much easier than if it was still using the facade (and in the controller):

 1 <?php  2     3 namespace Formativ;  4     5 use Mockery;  6 use TestCase;  7     8 class PostMailerTest  9 extends TestCase 10 { 11   public function tearDown() 12   { 13     Mockery::close(); 14   } 15    16   public function testSend() 17   { 18     $mailerMock = $this->getMailerMock(); 19    20     $mailerMock 21       ->shouldReceive("send") 22       ->atLeast()->once() 23       ->with( 24         "bar", ["baz"], 25         $this->getSendCallbackMock() 26       ); 27    28     $postMailer = new PostMailer($mailerMock); 29     $postMailer->send("foo", "bar", ["baz"]); 30   } 31    32   protected function getSendCallbackMock() 33   { 34     return Mockery::on(function($callback) { 35       $emailMock = Mockery::mock("stdClass"); 36    37       $emailMock 38         ->shouldReceive("to") 39         ->atLeast()->once() 40         ->with("foo"); 41    42       $callback($emailMock); 43    44       return true; 45     }); 46   } 47    48   protected function getMailerMock() 49   { 50     return Mockery::mock("Illuminate\Mail\Mailer"); 51   } 52 } 

This file should be saved as app/tests/Formativ/PostMailerTest.php.

Phew! Let’s break that up so it’s easier to digest…

1 Mockery::mock("Illuminate\Mail\Mailer"); 

Part of trying to test in the most isolated manner is substituting dependencies with things that don’t perform any significant function. We do this by creating a new mock instance, via the Mockery::mock() method.

We use this again with:

1 $emailMock = Mockery::mock("stdClass"); 

We can use stdClass the second time around because the provided class isn’t type-hinted. Our PostMailer class type-hints the IlluminateMailMailer class.

We then tell the test to expect that certain methods are called using certain arguments:

1 $emailMock 2   ->shouldReceive("to") 3   ->atLeast() 4   ->once() 5   ->with("foo"); 

This tells the mock to expect a call to an as-yet undefined to() method, and to expect that it will be passed “foo” as the first (and single) argument. If your production code expects a stubbed method to return a specific kind of data, you can add the andReturn() method.

We can provide expected callbacks, though it’s slightly trickier:

 1 Mockery::on(function($callback) {  2   $emailMock = Mockery::mock("stdClass");  3     4   $emailMock  5     ->shouldReceive("to")  6     ->atLeast()  7     ->once()  8     ->with("foo");  9    10   $callback($emailMock); 11    12   return true; 13 }); 

We set up a mock, which expects calls to it’s own methods, and then the original callback is run with the provided mock. Don’t forget to add return true - that tells mockery that it’s ok to run the callback with the mock you’ve set up.

At the end of all of this; we’re just testing that methods were called in the correct way. The test doesn’t worry about making sure the Laravel Mailer class actually sends the mail correctly - that has it’s own tests.

The repository class is slightly simpler to test:

 1 <?php  2     3 namespace Formativ;  4     5 use Mockery;  6 use TestCase;  7     8 class PostRepositoryTest  9 extends TestCase 10 { 11   public function tearDown() 12   { 13     Mockery::close(); 14   } 15    16   public function testSend() 17   { 18     $requestMock = $this->getRequestMock(); 19    20     $requestMock 21       ->shouldReceive("get") 22       ->atLeast() 23       ->once() 24       ->with("title"); 25    26     $requestMock 27       ->shouldReceive("get") 28       ->atLeast() 29       ->once() 30       ->with("subtitle"); 31    32     $requestMock 33       ->shouldReceive("get") 34       ->atLeast() 35       ->once() 36       ->with("body"); 37    38     $requestMock 39       ->shouldReceive("get") 40       ->atLeast() 41       ->once() 42       ->with("author"); 43    44     $postRepository = new PostRepository($requestMock); 45     $postRepository->insert(); 46   } 47    48   protected function getRequestMock() 49   { 50     return Mockery::mock("Illuminate\Http\Request"); 51   } 52 } 

This file should be saved as app/tests/Formativ/PostRepositoryTest.php.

All we’re doing here is making sure the get method is called four times, on the request dependency. We could extend this to accommodate requests against the underlying database connector object, and the test code would be similar.

We can’t completely test the Str::slug() method because it’s not a facade but water a static method on the Str class. Every facade allows you to mock methods (facades subclass the MockObject class), and you can even swap them out with your own mocks (using the Validator::swap($validatorMock) method).

You can test static method calls using the AspectMock library, which you can learn more about at: .

You can learn more about facades at: .

Finally, let’s test the controller:

  1 <?php   2      3 class PostControllerTest   4 extends TestCase   5 {   6   public function tearDown()   7   {   8     Mockery::close();   9   }  10     11   public function testConstructor()  12   {  13     $repositoryMock = $this->getRepositoryMock();  14     15     $mailerMock = $this->getMailerMock();  16     17     $dispatcherMock = $this->getDispatcherMock();  18     19     $dispatcherMock  20       ->shouldReceive("listen")  21       ->atLeast()  22       ->once()  23       ->with(  24         "post.store",  25         [$repositoryMock, "insert"]  26       );  27     28     $dispatcherMock  29       ->shouldReceive("listen")  30       ->atLeast()  31       ->once()  32       ->with(  33         "post.store",  34         [$mailerMock, "send"]  35       );  36     37     $postController = new PostController(  38       $repositoryMock,  39       $this->getValidatorMock(),  40       $mailerMock,  41       $this->getResponseMock(),  42       $dispatcherMock  43     );  44   }  45     46   public function testStore()  47   {  48     $validatorMock = $this->getValidatorMock();  49     50     $validatorMock  51       ->shouldReceive("passes")  52       ->atLeast()  53       ->once()  54       ->with("store")  55       ->andReturn(true);  56     57     $responseMock = $this->getResponseMock();  58     59     $responseMock  60       ->shouldReceive("route")  61       ->atLeast()  62       ->once()  63       ->with("posts.index")  64       ->andReturn($responseMock);  65     66     $responseMock  67       ->shouldReceive("with")  68       ->atLeast()  69       ->once()  70       ->with("success", true);  71     72     $dispatcherMock = $this->getDispatcherMock();  73     74     $dispatcherMock  75       ->shouldReceive("fire")  76       ->atLeast()  77       ->once()  78       ->with("post.store");  79     80     $postController = new PostController(  81       $this->getRepositoryMock(),  82       $validatorMock,  83       $this->getMailerMock(),  84       $responseMock,  85       $dispatcherMock  86     );  87     88     $postController->store();  89   }  90     91   public function testStoreFails()  92   {  93     $validatorMock = $this->getValidatorMock();  94     95     $validatorMock  96       ->shouldReceive("passes")  97       ->atLeast()  98       ->once()  99       ->with("store") 100       ->andReturn(false); 101    102     $validatorMock 103       ->shouldReceive("messages") 104       ->atLeast() 105       ->once() 106       ->with("store") 107       ->andReturn(["foo"]); 108    109     $responseMock = $this->getResponseMock(); 110    111     $responseMock 112       ->shouldReceive("back") 113       ->atLeast() 114       ->once() 115       ->andReturn($responseMock); 116    117     $responseMock 118       ->shouldReceive("withErrors") 119       ->atLeast() 120       ->once() 121       ->with(["foo"]) 122       ->andReturn($responseMock); 123    124     $responseMock 125       ->shouldReceive("withInput") 126       ->atLeast() 127       ->once() 128       ->andReturn($responseMock); 129    130     $postController = new PostController( 131       $this->getRepositoryMock(), 132       $validatorMock, 133       $this->getMailerMock(), 134       $responseMock, 135       $this->getDispatcherMock() 136     ); 137    138     $postController->store(); 139   } 140    141   protected function getRepositoryMock() 142   { 143     return Mockery::mock("Formativ\PostRepositoryInterface") 144       ->makePartial(); 145   } 146    147   protected function getValidatorMock() 148   { 149     return Mockery::mock("Formativ\PostValidatorInterface") 150       ->makePartial(); 151   } 152    153   protected function getMailerMock() 154   { 155     return Mockery::mock("Formativ\PostMailerInterface") 156       ->makePartial(); 157   } 158    159   protected function getResponseMock() 160   { 161     return Mockery::mock("Illuminate\Support\Facades\Response") 162       ->makePartial(); 163   } 164    165   protected function getDispatcherMock() 166   { 167     return Mockery::mock("Illuminate\Events\Dispatcher") 168       ->makePartial(); 169   } 170 } 

This file should be saved as app/tests/controllers/PostControllerTest.php.

Here we’re still testing method calls, but we also test multiple paths through the store() method.

We haven’t used any assertions, even though they are very useful for unit and functional testing. Feel free to use them to check output values…

You can find out more about Mockery at: .

You can find out more about PHPUnit at: .

The Rabbit Hole

The process of test-writing can take as much time as you want to give it. It’s best just to decide exactly what you need to test, and step away after that.

We’ve just looked at a very narrow area of testing, in our applications. You’re likely to have a richer data layer, and need a ton of tests for that. You’re probably going to want to test the rendering of views.

Don’t think this is an exhaustive reference for how to test, or even that this is the only way to functionally test your controller code. It’s simply a method I’ve found works for the applications I write.

Alternatives

The closest alternative to testing with PHPUnit is probably PHPSpec (). It uses a similar dialect of assertions and mocking.

If you’re looking to test, in a broader sense, consider looking into Behat (). It uses a descriptive, text-based language to define what behaviour a service/library should have.

Назад: File-Based CMS
На главную: Предисловие