Laravel 8 Writing Unit/Feature Tests Part-2

Writing first Unit test using Laravel 8 framework

In my last tutorial you learn basics about unit tests. If you have not read my last article please do read it will help you grasp some important concepts if you follow my tutorial in order.

Getting Started with Laravel 8 Unit Tests

First of all in order to get started with unit test you need to download brand new Laravel 8 project locally. I have setup my local environment using Laravel Sail and throughout over tutorial we will run all our commands using Laravel Sail.

To set up new Laravel 8 project using sail follow my tutorial on Laravel sail:

Setup Laravel 8 Project Using Laravel Sail

What is Unit Test in Laravel?

Before we jump into coding it is important to understand the defination of Unit test. If you do not understand the concept you will be writing unit tests in wrong way. Unit tests are basically used to test isolated functionality which is independent of core laravel service.

For example: if you write your custom classes or function withing your laravel application and you want to test these functions or classes you can use Unit tests rahter then using Feature tests.

Laravel Unit test do not boot up Laravel application therefore in your unit tests you will not be able to use any core laravel functionality. For example: controllers, models, database connection etc wont work properly.

What can you test under Unit Testing?

You can test following things under unit testing:

  • Custom classes
  • Custom functions
  • Custom service classes that do not contain any core laravel services inside your function or class

How to efficiently write unit tests?

I am going to use my expertise to explain how you can write efficient unit tests. I always create unit test first and then add make it fail then I will improve my test to pass all test cases.

In other word, make your test to fail first and then add different uses cases to make it pass. In order to understand what I actually mean let's take one practical example.

Let's create a new class called UrlHelper we will write some function inside our class and for each function we write inside the class we will write unit test. Let's create a directory called Helpers under app folder in your laravel project.

Let's create a new function called extractLinksFromString inside our UrlHelper class. The purpose of our function is following:

  • Extract all links inside a given string
  • Prepare the array of links
  • Return the array with links

Here is the sample class look like in the begining we will implement our function late in this tutorial.

<?php

namespace App\Helpers;

class UrlHelper
{
    /**
     * @param string $text
     * @return array
     */
    public function extractLinksFromString(string $text): array
    {
        if (!$text) {
            return [];
        }
        return [];
    }
}​

Whenever I create a new helper class or function I always create unit test for it. Therefore, we will now create first unit test go to your project root folder and using Laravel sail run following command in your terminal window to create our first unit test.

./vendor/bin/sail artisan make:test UrlHelperTest --unit​

Above command will create a new file under tests/Unit/UrlHelperTest.php file. Let's open this file and modify our test function as below:

<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class UrlHelperTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testExtractLinksFromString()
    {
        $this->assertTrue(false);
    }
}​

I follow camelcases name and prefix my tests using test keyword. At this point we have UrlHelper class with one function and we have created one unit test for our helper class.

As I said earlier I always set my test case to fail first and then I improve the test and also I implement my original function at the same time. Let's modify our test slightly.

Our unit test will create a new class object and then call our function and will check to see if our function will return blank array to pass our first case. Our function should return blank array when we supply empty string.

Open our unit test file and modify our test as below:

<?php

namespace Tests\Unit;

use App\Helpers\UrlHelper;
use PHPUnit\Framework\TestCase;

class UrlHelperTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testExtractLinksFromString()
    {
        $urlHelper = new UrlHelper();

        // UseCase-1: our function should return blank array when blank string is given
        $this->assertEmpty($urlHelper->extractLinksFromString(''));
    }
}​

Alright, our first use case is that when we supply blank string to our function it should return blank array. Therefore above test should pass as our original function just returns blank array.

Open your terminal window and run following command to see if our test passes:

./vendor/bin/sail test ./tests/Unit​

When you run above command it will output as below:

Implement code as we write unit tests

Now, our second usecase is when a string is given with one link inside the string our function should return single array element. First of all we will change our unit test according to our new use case and we will make sure that it will fail first.

Once you know that your test is failing you need to make your test pass by implementing a new code inside your original function according to new usecase that we are given,

Open your unit test file and write this new usecase and make it fail first. Also do not remove our previous use case we want to make sure our function covers multiple usecase so that we can efficiently test our code.

<?php

namespace Tests\Unit;

use App\Helpers\UrlHelper;
use PHPUnit\Framework\TestCase;

class UrlHelperTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testExtractLinksFromString()
    {
        $urlHelper = new UrlHelper();

        // UseCase-1: our function should return blank array when blank string is given
        $this->assertEmpty($urlHelper->extractLinksFromString(''));

        // UseCase-2: our function returns single link
        $stringWithSingleLink = "This is as test link https://learn2torials.com";
        $this->assertCount(1, $urlHelper->extractLinksFromString($stringWithSingleLink));
    }
}​

Alright as you can see we added usecase-2 write after our usecase-1. We introduced a new string variable which contains exactly one link within a string. Our test now calls our original function by passing this new string variable and we are asserting that what ever is returned from the function matches one single array element.

assertCount function basically checks the number of elements an array returns our function right now returns zero element therefore above test will fail. Let's run our unit test to make sure it fails and then we will implement our original function so that our new usecase passes.

Open your terminal and run following command:

./vendor/bin/sail test ./tests/Unit​

When you run above command it will output something like:

Alright, now it's time to modify our original function so that it passes our both use cases. Open your UrlHelper class and add following code:

<?php

namespace App\Helpers;

class UrlHelper
{
    /**
     * @param string $text
     * @return array
     */
    public function extractLinksFromString(string $text): array
    {
        if (!$text) {
            return [];
        }

        preg_match_all('!https?://\S+!', $text, $matches);

        return [
            $matches[0][0] ?? []
        ];
    }
}​

Now, you run our test again to see if passes both our usecases. Open your terminal and run below command again:

./vendor/bin/sail test ./tests/Unit​

It will output following screen:

Writing unit test without breaking our business logic

Let say that your function mets two usecases so far when your code tested by some other developer it fails for them. They say your function is not working when string contains multiple urls in one string.

You have not tested this scenario yet however you go in and modify your test to cover use case-3 when multiple urls are given within  a string. First of all you will add third use case in your unit test file and watch it fail.

You open your unit test and add another usecase to existing unit test file as below:

<?php

namespace Tests\Unit;

use App\Helpers\UrlHelper;
use PHPUnit\Framework\TestCase;

class UrlHelperTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function testExtractLinksFromString()
    {
        $urlHelper = new UrlHelper();

        // UseCase-1: our function should return blank array when blank string is given
        $this->assertEmpty($urlHelper->extractLinksFromString(''));

        // UseCase-2: our function returns single link
        $stringWithSingleLink = "This is as test link https://learn2torials.com";
        $this->assertCount(1, $urlHelper->extractLinksFromString($stringWithSingleLink));

        // UseCase-3: our function should be able to return multiple urls
        $stringWithSMultipleLinks = "This is as test link https://learn2torials.com and https://google.com";
        $this->assertCount(2, $urlHelper->extractLinksFromString($stringWithSMultipleLinks));
    }
}​

After you added a new use case in your unit test file let's run the test to make sure it fails first before we implement our original function. Open your terminal and run following command and check the output of your test.

./vendor/bin/sail test ./tests/Unit​

Above command will result in following output:

Now, that we know our function is not yet complete and does not cover usecase-3 we modify our function slightly so that it will cover all three usecases without breaking other two usecases.

You can also see that as usecases are growing our function is also getting ugly. You will also realize that you can write this function more efficiently then before it will also help you detect code errors and quality of your code.

This time we will change our function in such a way so that it is not ugly and covers all usecases that you can think of. Open your UrlHelper and modify your function as below:

<?php

namespace App\Helpers;

class UrlHelper
{
    /**
     * @param string $text
     * @return array
     */
    public function extractLinksFromString(string $text): array
    {
        preg_match_all('!https?://\S+!', $text, $matches);

        return $matches[0];
    }
}​

Now, our function looks much cleaner and it will pass all our test cases. Run your test again using following command:

./vendor/bin/sail test ./tests/Unit​

It will output as below:

That's all in this tutorial. What did we learn today?

  • How to write unit test in Laravel framework?
  • How can we efficiently improve our code by making our test fail first and then make it pass.
  • How can we write tests without affecting other business logic.

Hope I was able to explain the concept in nicer way please like and share this article if you feel I was able to deliver some good knowledge.