Starting out with Unit Testing: Laravel
I had been shying away from writing Unit tests until I started writing them regularly from the last 6 months or so.
I was under the impression that Unit Tests just add to overall time needed to complete a task and my teammates also agreed with me.
A smaller project which you work on for 6 months and then it simply vanishes into the crowd of internet apps and you never get to work on it again, then you won’t ever see the value of those test cases.
To actually understand the value of Unit Tests you need to have a project with following traits:
- Considerably large project with longer project life and goes under upgrades and changes during that life time.
- Different developers work on it during that lifetime.
Also I was able to appreciate having Unit Tests when I compared two projects which satisfied the above two conditions, while Project 1 didn’t have any Unit Tests and Project 2 had them.
Following were the issues that we faced in Project 1(without Unit Test cases)
1. New Developers didn’t understand the impact of their changes
I worked on that project for at least 4 years and I saw at least 6 other developers come and go on the project. Since the codebase was huge, it wasn’t easy for them to understand how the project code was interconnected.
We developers start to become nervous if we cannot contribute to the codebase sooner, so they didn’t take the time to understand the entire related workflow. This eagerness to push code, lead to defects in other places which no one noticed until a bug was filed by a customer or discovered by the project owner.
2. Manual testing took more time
As the confidence of the project owner decreased with usual defects and bugs after every release. Manual testing was made more rigorous, regression was done on important application features which was redundant.
3. Dwindling Developer confidence and interest
Some fellow developers had already started blaming bad project documentation as the culprit ( which I believe would have become huge if it was there and no one would still bothered going through it). Also since nobody wants to stay in a sinking ship, they didn’t want to continue working on the project.
Which lead them to leave the project, leading to the vicious cycle of a new developer coming in and making the same kind of mistakes, hurting the project even more.
Now with the project that had Unit Test Cases the impact of such things was greatly reduced. Even if you have 50% code coverage it is a huge thing for the maintainability of the project.
The benefits of having Unit tests:
- Better maintainability because the test cases made sure that the changes didn’t impact the functionality that was already working.
- The test cases served as the documentation for the code because having test cases named like
it_doesnt_send_email_when_user_is_verified
tells the developer a little bit about the business cases. - Less rework.
- Reduced time on manual testing.
- Higher developer confidence as they can test their own changes while making sure they didn’t break anything.
- Saves project development time in the long run.
How to start writing Unit tests?
If you are using (PHP)Laravel it comes baked in and it is really very easy to get started. Just head over to the tests directory in your project and you should see some ExampleTest setup for you already. They are good for starting out.
I usually run the following command
php artisan make:test --unit Http\\Controllers\\AdminTools\\ReassignUserControllerTest
Here I am testing \App\Http\Controllers\AdminTools\ReassignUserController
and I usually map class namespace in app to the namespace under the test suite(Unit or Functional). I haven’t checked if this is the standard but this approach helps me see if a test case has been implemented for a particular class already or not
Once you have the test file, now you have to decide what you need to test. I would usually write tests after I implement a piece of functionality so it is not TDD per se, but I like this approach better because I now can take decision easily on :
- What functionality to Mock
- What data needs to be set up.
Mocking and Data Setup are needed to test different use cases. Use Cases in turn are verified using assertions. Assertions can be simply stated as a method that allows you to check for a particular condition.
I think an example can explain it well and you can start from there
Below is a test for confirming that when a user is deleted. The deleted user’s email address should be removed from all the other user’s alert field.
So we hit our URL for deleting a user which is DELETE hostname/user/:id
<?php namespace Tests\Feature; use App\User; use Tests\TestCase; use Illuminate\Foundation\Testing\DatabaseTransactions; class UsersControllerTest extends TestCase { use DatabaseTransactions; /** * @test * * @return void */ public function it_removes_email_of_deleted_user_from_alert_field() { $user = User::where('username', 'superuser')->first(); $deletedUserEmail = 'deleted@test.com'; $deleteUser = factory(\App\User::class)->create([ 'name' => 'Deleted User', 'email' => $deletedUserEmail ]); $user1 = factory(\App\User::class)->create([ 'alert' => 'a@a.com, ' . $deletedUserEmail . ', b@b.com', ]); $user2 = factory(\App\User::class)->create([ 'alert' => $deletedUserEmail . ', a@a.com, b@b.com', ]); $user3 = factory(\App\User::class)->create([ 'alert' => 'a@a.com, b@b.com, ' . $deletedUserEmail, ]); $this->assertStringContainsString($deletedUserEmail, $user1->alert); $this->assertStringContainsString($deletedUserEmail, $user2->alert); $this->assertStringContainsString($deletedUserEmail, $user3->alert); $response = $this->actingAs($user)->delete('/users/' . $deleteUser->id); $user1->refresh(); $user2->refresh(); $user3->refresh(); $this->assertStringNotContainsString($deletedUserEmail, $user1->alert); $this->assertStringNotContainsString($deletedUserEmail, $user2->alert); $this->assertStringNotContainsString($deletedUserEmail, $user3->alert); $response->assertStatus(200); } }
Let’s try a line by line explanation to help you :
Line 9: Extend TestCase
class to get many laravel specific functionality in your tests along with many useful assertions
Line 11: Use a Trait which will use transactions when running your tests. Useful when you don’t use a separate DB for testing, it will run the tests and then remove all the created data when it finishes
Line 17: Name of the test. Keep it as descriptive as possible
Line 19: Fetching a user from DB. Will later use this user to perform certain actions
Line 22-37: Creating a new user. This is where Factories(Data Setup) come in. You define your factories so that you can quickly create dummy data for your tests. Laravel ships with a sample factory which can be found at database/factories/UserFactory.php
Use this to build other model factories as well.
Line 39-42: Doing an assertion before hitting the delete URL. Just to make sure that the “to be deleted” user’s email address exists in the alert field of the other 3 users that we created above.
Line 43: We hit the URL actingAs
the superuser(check Line 19)
Line 49-51: We assert that the deleted user’s email address is removed from the other users’ alert field
I hope this helps get you started, Unit testing might sound daunting at first but believe me with modern frameworks it is pretty easy. You can even Unit test your frontend code. I would try our Unit testing with my Vue applications and will write about that as well.