By Terry Rowland, Senior Backend Web Developer at Enola Labs.
Automated testing is critical to my method of development. When I first started off using Laravel, the way it did dependency injection via the constructor and the method for controllers was like magic to me and caused quite a bit of confusion in testing. So, with this post, I hope to clear up some of that confusion by explaining how the controller and Service Container work together and how to leverage the container for testing purposes.
I’m going to be showing all my examples in Laravel 5.2.31 and I’ll be using Laravel Homestead (Vagrant) to build everything. Here is a link to the repository where you can grab the code. As a side note, if you are familiar with Laravel, don’t run the migrations. In this example, I purposely want errors to occur to show we shouldn’t be hitting the DB.
First, I will create the route I want to use in the app/routes.php file.
Route::get('/api/users', 'UsersController@index') ->name('api.users.index');
Now, I’m going to use a repository, below, specifically for example purposes, but I wouldn’t suggest you use one in this case. Typically, repositories are reserved for more complex queries and logic, so using one here would more than likely be overkill. So, let’s create a folder under app, called “Repositories”, and place the following code in the file named “UserRepository.php”.
<?php namespace AppRepositories; use AppUser; class UserRepository { /** @var User */ protected $userModel; public function __construct(User $user) { $this->userModel = $user; } /** * Gets all the users in the system. * * @return IlluminateDatabaseEloquentCollection|null */ public function all() { return $this->userModel->all(); } }
This code will work, but I don’t plan on using it. I’ll also say that this code would be tested if it were going into one of my applications, but for the purpose of this, I want to make sure the “interface” and intention of the class is fleshed out first.
Next, let’s build the controller we will be using for the route we created. In the app/Http directory, create a file named UsersController.php and place the following code in it:
<?php namespace AppHttpControllers; use AppRepositoriesUserRepository; class UsersController extends Controller { public function index(UserRepository $userRepo) { return $userRepo->all(); } }
This is simple code here—just an index function plus leveraging Laravel Service Container to get a built-out UserRepository so we can return its results.
Next, I will jump over to my test and start the process of building this out. My focus here is on creating the basic shell of what I know is minimally needed to make everything work.
<?php class ExampleTest extends TestCase { public function testUserRepositoryIsCalledToGetAllUsers() { // This is one way Laravel calls routes that are // registered to the application. $this->get('/api/users'); } }
In the Vagrant VM, and in the application directory, if I were to run the command “phpunit”, I would actually (attempt to) hit a database. You would get a green, but behind the scenes you are actually getting an error that was swallowed up. If you would like to see the error, drop in this chunk of code:
dd($this->response->getContent());
after the:
$this->get('/api/users');
This is expected. I purposely did NOT run the migrations, as mentioned earlier, but if you look at the error (or look in the storage/logs/laravel.log), you will see there’s a query exception about the users table not existing. So, with this information we KNOW the repository is actually being used and it’s actually working—attempting to hit the DB with a query on the users table.
Be sure to remove that dd code because it will no longer be necessary and will interfere with our test later.
So, now we can work up a little more code in the test:
<?php use AppRepositoriesUserRepository; class ExampleTest extends TestCase { /** @var UserRepository|MockeryMockInterface */ protected $mockedUserRepo; public function setUp() { parent::setup(); $this->mockedUserRepo = Mockery::mock(UserRepository::class); app()->instance(UserRepository::class, $this->mockedUserRepo); } public function testUserRepositoryIsCalledToGetAllUsers() { $$this->mockedUserRepo ->shouldReceive('all') ->once() ->withNoArgs() ->andReturnNull(); $this->get('/api/users'); } }
Let me break this down a little.
I like being explicit in my tests, so I added the mockedUserRepo variable for clarity and type hinting. For example, down in the testing function when I use the arrow in my IDE (PHPStorm), it shows all the possible functions I can use from the repository AND the mock because of how I used the pipe character (|) in the comment.
Second, I built up a setup function. This function must be public and it’s VERY important to call the parent::setup() because if we don’t, the Laravel application will not be built and we will get several errors.
Third, I created a mocked version of the repository with the code $this->mockedUserRepo = Mockery::mock(UserRepository::class), then assigned it to the Service Container as the same name (the first parameter—”UserRepository::class”) with the code app()->instance(UserRepository::class, $this->mockedUserRepo).
This tells the Service Container to create an entry in the container with the “name” of “AppRepositoriesUserRepository”, and assign the given class to that name. In this case, it’s the mocked UserRepository. Now, even if the name “AppRepositoriesUserRepository” already exists in the container, it will replace it with the mocked version. Also note, this will only happen for each test. So, if I made a new test and didn’t mock the repository, it would go back to hitting the database.
Finally, we can add the code that is going to do the “checking” that we are using the mock properly:
$this->mockedUserRepo ->shouldReceive('all') ->once() ->withNoArgs() ->andReturnNull();
If you aren’t familiar with Mockery, it’s okay; you can learn more here. But, what’s great about Mockery is that it’s super simple to read. Basically, we should expect the all function to be called once, with no arguments given to the function and then return null as a result of that “mocked” call.
Now, if we run “phpunit” in the terminal, we should get something similar to this:
Figure 1: The result of running “phpunit” in the terminal
Notice there are 0 assertions; this is expected, but a little confusing. There are assertions going on, but only in Mockery at this point. If you were to comment out the
$this->mockedUserRepo ->shouldReceive('all') ->once() ->withNoArgs() ->andReturnNull();
you would see Mockery bark, saying “Method Mockery_0_App_Repositories_UserRepository::all() does not exist on this mock object”. This would be swallowed up again, but by using the method mentioned earlier, using
dd($this->response->getContent());
you can see it. And, as one last precaution of not getting one of these silent errors, you can replace:
$this->get('/api/users');
with:
$this->get('/api/users'); $this->assertEmpty($this->response->getContent());
And now, any time there’s a “break” in the test, this will catch it because we have the mock returning null! When I first picked up Laravel, this threw me for a loop. I hope I was able to help demystify some of Laravel’s magic in the controllers.
P.S. The method will also work just the same if the constructor is used for injection. You can try it by replacing your controller code with this code:
<?php namespace AppHttpControllers; use AppRepositoriesUserRepository; class UsersController extends Controller { /** UserRepository */ protected $userRepo; public function __construct(UserRepository $userRepo) { $this->userRepo = $userRepo; } public function index() { return $this->userRepo->all(); } }
Good luck out there!
About the Author
Terry Rowland is a Senior Backend Web Developer at Enola Labs, a custom Web and mobile application development company located in Austin, Texas.
*** This article was contributed for exclusive publication on this site. ***