⬆️ ⬇️

Unit testing in Laravel

I often hear among community discussions the opinion that unit testing in Laravel is wrong, difficult, and the tests themselves are long and giving no benefit. Because of this, few people write these tests, limiting themselves only to feature tests, and the utility of unit tests tends to 0.

I also thought so once, but once I thought about it and asked myself - maybe I don’t know how to cook them?



For a while I understood and at the exit I had a new understanding of unit tests, and the tests became clear, friendly, fast and began to help me.

I want to share my understanding with the community, and even better to understand this topic, to make my tests even better.



Some philosophy and limitations



Laravel - in some places a kind of framework. Especially in terms of facades and eloquent. I will not touch upon discussions or convictions of these moments, but I will show how I combine them with unit tests.

I write tests after (or simultaneously) writing the main code. My approach may not be compatible with the TDD approach or require partial adjustments.



The main question I ask myself before writing a test is “what exactly do I want to test?”. This is an important question. This very thought allowed me to reconsider my views on writing unit tests and the project code itself.



Tests should be stable and minimally dependent on the environment. If you make mutations, your tests fail, most likely they are good. Conversely, if they do not fall, they are probably not very good.



Out of the box Laravel supports 3 types of tests:





I will talk mainly about Unit tests.



I am not testing all code through unit tests (perhaps this is not correct). I am not testing some code at all (more on this below).



If moki are used in tests, do not forget to do Mockery :: close () on tearDown.



Some examples of tests are "taken from the Internet."



How I test



Below I will group test examples by class groups and try to give test examples for each class group. For most groups of classes, I will not give examples of the code itself.



Middleware



For the unit test middleware, I create an object of class Request, an object of the desired Middleware, then I call the handle method and execute the necessary asserts. Middleware by the actions performed can be divided into 3 groups:





Suppose that we have the following Middleware, whose task is to modify the title field:



class TitlecaseMiddleware { public function handle($request, Closure $next) { if ($request->title) { $request->merge([ 'title' => title_case($request->title) ]); } return $next($request); } } 


A test for such Middleware might look like this:



 public function testChangeTitleToTitlecase() { $request = new Request; $request->merge([ 'title' => 'Title is in mixed CASE' ]); $middleware = new TitlecaseMiddleware; $middleware->handle($request, function ($req) { $this->assertEquals('Title Is In Mixed Case', $req->title); }); } 


Tests for groups 2 and 3 will be such a plan, respectively:



 $response = $middleware->handle($request, function () {}); $this->assertEquals($response->getStatusCode(), 302); //   $this->assertEquals($response, null); //      request 


Request class



The main task of this class group is to authorize and validate requests.



I do not test these classes through unit tests (I admit that this may not be true), only through feature tests. In my opinion, unit tests are redundant for these classes, but I found some interesting examples of how this can be done. Perhaps they will help you if you decide to test your request class unit with tests:





Controller



I also do not test controllers through unit tests. But when testing them, I use one feature that I would like to talk about.



Controllers, in my opinion, should be light. Their task is to get the right request, call the necessary services and repositories (since both of these terms are “alien” for Laravel, I will give an explanation according to my terminology), return the answer. Sometimes trigger an event, a job, etc.

Accordingly, when testing through feature tests, we need not only to call the controller with the necessary parameters and check the answer, but also to lock the necessary services and check that they are actually being called (or not being called). Sometimes - create a record in the database.



An example of a controller test with mock class of service:



 public function testProductCategorySync() { $service = Mockery::mock(\App\Services\Product::class); app()->instance(\App\Services\Product::class, $service); $service->shouldReceive('sync')->once(); $response = $this->post('/api/v1/sync/eventsCallback', [ "eventType" => "PRODUCT_SYNC" ]); $response->assertStatus(200); } 


An example of a controller test with a facades mock (in our case, an event, but by analogy is done for other Laravel facades):



 public function testChangeCart() { Event::fake(); $user = factory(User::class)->create(); Passport::actingAs( $user ); $response = $this->post('/api/v1/cart/update', [ 'products' => [ [ // our changed data ] ], ]); $data = json_decode($response->getContent()); $response->assertStatus(200); $this->assertEquals($user->id, $data->data->userId); // and assert other data from response Event::assertDispatched(CartChanged::class); } 


Service and Repositories



There are no out of box data types. I try to keep the controllers thin, so I put all the extra work into one of these groups of classes.



I defined the difference between them as follows:





For the Repository classes, I almost do not write tests.



An example of the Service class test below:



 public function testUpdateCart() { Event::fake(); $cartService = resolve(CartService::class); $cartRepo = resolve(CartRepository::class); $user = factory(User::class)->make(); $cart = $cartRepo->getCart($user); // set data $data = [ ]; $newCart = $cartService->updateForUser($user, $data); $this->assertEquals($data, $newCart->toArray()); Event::assertDispatched(CartChanged::class, 1); } 


Event-Listener, Jobs



These classes are tested almost according to the general principle - we prepare the data necessary for testing; Call the required class from the framework and check the result.

Example for Listener:



 public function testHandle() { $user = factory(User::class)->create(); $cart = Cart::create([ 'userId' => $user->id, // other needed data ]); $listener = new CreateTaskForSyncCart(); $listener->handle(new CartChanged($cart)); $job = // get our job $this->assertSame(json_encode($cart->products), $job->payload); $this->assertSame($user->id, $job->user_id); // some additional asserts. Work with this data simplest for example $this->assertTrue($updatedAt->equalTo($job->last_updated_at)); } 


Console commands



I see console commands as some kind of controller that can additionally output (and produce more complex manipulations with console input / output described in the documentation) data. Accordingly, tests are obtained similar to the controller: we check that the necessary methods of services are triggered, events are triggered (or not), and we also check the interaction with the console (output or request data).



An example of such a test:



 public function testSendCartSyncDataEmptyJobs() { $service = m::mock(CartJobsRepository::class); app()->instance(CartJobsRepository::class, $service); $service->shouldReceive('getAll') ->once()->andReturn(collect([])); $this->artisan('sync:cart') ->expectsOutput('Get all jobs for sending...') ->expectsOutput('All count for sending: 0') ->expectsOutput('Empty jobs') ->assertExitCode(0); } 


Separate external libraries



As a rule, if separate libraries have features for unit tests, they are described in the documentation. In other cases, work with this code is tested in the same way as the service layer. Libraries themselves do not make sense to cover with tests (only if you want to send PR to this library) and should be considered as some kind of black box.



On many projects I have to interact through API with other services. In Laravel, the Guzzle library is often used for this purpose. It seemed to me convenient to bring all the work with other services into a separate class of the NetworkService service. This made it easier for me to write and test the core code, helped standardize responses and error handling.



I give examples of several tests for my NetworkService class:



 public function testSuccessfulSendNetworkService() { $mockHandler = new MockHandler([ new Response(200), ]); $handler = HandlerStack::create($mockHandler); $client = new Client(['handler' => $handler]); app()->instance(\GuzzleHttp\Client::class, $client); $networkService = resolve(NetworkService::class); $response = $networkService->sendRequestToSite('GET', '/'); $this->assertEquals('200', $response->getStatusCode()); } public function testUnsupportedMethodSendNetworkService() { $networkService = resolve(NetworkService::class); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToSite('PUT', '/'); } public function testUnsetConfigUrlNetworkService() { $networkService = resolve(NetworkService::class); Config::shouldReceive('get') ->once() ->with('app.api_url') ->andReturn(''); Config::shouldReceive('get') ->once() ->with('app.api_token') ->andReturn('token'); $this->expectException('\InvalidArgumentException'); $networkService->sendRequestToApi('GET', '/'); } 


findings



This approach allows me to write better and more understandable code, to take advantage of the SOLID and SRP approaches when writing code. My tests have become faster, and most importantly - they began to bring me a favor.



With active refactoring when expanding or changing functionality, we immediately see what exactly falls and we can quickly and precisely fix errors without releasing them from the local environment. This makes error correction as cheap as possible.



I hope that the principles and approaches I have described will help you understand unit testing in Laravel and make unit tests your helpers in code development.



Write your additions and comments.



')

Source: https://habr.com/ru/post/457866/



All Articles