Guides
Unit Testing in Laravel

How to Get Started with Unit Testing in Laravel

Better Stack Team
Updated on June 21, 2022

Testing is an important phase in the software development life cycle. It ensures that the code you've written is working as designed before you move on to the next part of your project. Every time you finish writing a software component, you must also write a test to verify that its behavior matches your expectations. This process helps maintain the quality of your code.

When we talk about testing in Laravel, we usually mean two things: unit testing and feature testing. Unit testing only focuses on a small piece of code (usually a method inside a class), while a feature tests verifies if a particular feature, which may consist of multiple methods or classes interacting with each other, is working the way you designed.

In this tutorial, we are going to talk about how to write unit tests in a Laravel project using PHPUnit, the most popular unit testing package for PHP projects. It is well integrated with Laravel, so no additional configuration is required after you create a new Laravel project.

One thing to note is that even though PHPUnit is originally designed for unit testing, it is completely okay if you use it for feature testing as well. The package includes lots of powerful assertions that can be useful in many different testing scenarios.

By following through with this tutorial, you will learn the following aspects of unit testing in Laravel:

  • Creating and running tests.
  • Understanding how to use PHPUnit assertions.
  • Testing HTTP servers.
  • Testing both JSON and HTML APIs.
  • How to generate fake data for testing purposes.
  • Understanding browser testing with Laravel Dusk.
  • Mocking in Laravel.
  • Setting up a continuous integration test workflow.

Prerequisites

Before you proceed with this tutorial, ensure that you have met the following requirements:

  • You have a basic understanding of PHP.
  • You have a code editor (such as Visual Studio Code) installed on your computer.
  • You have PHP (v8.0 or later) installed on your machine.
  • If you are using a Windows PC, make sure you have WSL2 and Docker Desktop installed on your machine.
  • If you are using Mac, you only need to install Docker Desktop.
  • If you are using Linux, please install Docker Compose.
  • You have Google Chrome installed for browser testing through Laravel Dusk.
  • Sign up for a GitHub account if you don't have one already. We'll be using it to set up a CI/CD workflow for testing in the final step of this article.

Step 1 — Creating a new Laravel project

Let's start by creating a new Laravel project from scratch. The Laravel team has provided a tool called Laravel Sail that allows us to quickly set up a project, no matter what operating system you are using, as long as you have Docker installed and running.

Open the terminal and run the following command to create a new project:

curl -s https://laravel.build/<your_project_name> | bash
Copied!

This will create a new Laravel project under the directory where you executed the command. Next, go into the project you just created:

cd <your_project_name>
Copied!

In the project directory, run the sail up command as shown below. Ensure to stop any instance of Redis or MySQL if they are running on your machine, otherwise, you may get some errors about ports not being available.

./vendor/bin/sail up
Copied!

Laravel Sail will automatically install everything you need inside a Docker container, such as a database, Redis, and even a mail server, and all we have to do is wait. It could take several minutes to build the first time, but subsequent builds will be much faster. If you see the following output, it means that Laravel Sail was launched successfully.

Output
Starting code_mailhog_1 ...
Starting code_mailhog_1     ... done
Starting code_redis_1       ... done
Starting code_meilisearch_1 ... done
Starting code_mysql_1       ... done
Creating code_laravel.test_1 ... done
. . .
laravel.test_1 | Starting Laravel development server: http://0.0.0.0:80
laravel.test_1 | [Thu Jun 2 17:37:32 2022] PHP 8.1.5 Development Server (http://0.0.0.0:80) started

Step 2 — Examining the default test setup in Laravel

Before we start writing tests, let's take a look at the default tests that Laravel has provided for us. It will help us understand how testing works in Laravel.

The phpunit.xml file

There is a phpunit.xml file in the project root directory. It is the configuration file for PHPUnit, a testing framework for PHP applications. It is divided into three main sections as shown below:

phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>
    </testsuites>
    <coverage processUncoveredFiles="true">
        <include>
            <directory suffix=".php">./app</directory>
        </include>
    </coverage>
    <php>
        <env name="APP_ENV" value="testing" />
        <env name="BCRYPT_ROUNDS" value="4" />
        <env name="CACHE_DRIVER" value="array" />
        <env name="DB_DATABASE" value="testing" />
        <env name="MAIL_MAILER" value="array" />
        <env name="QUEUE_CONNECTION" value="sync" />
        <env name="SESSION_DRIVER" value="array" />
        <env name="TELESCOPE_ENABLED" value="false" />
    </php>
</phpunit>
Copied!

In the <testsuites> section, two different types of tests are predefined for us. This section tells PHPUnit to run the tests that are stored in the ./tests/Unit and ./tests/Feature directories. We'll take a closer look at both directories in the next step of this tutorial.

Next, the <coverage> section is where we define what code and what directories are covered in the test, and how they are covered. It is not important for this tutorial so we can leave it as is.

Finally, in the <php> section, we can define environment variables for the testing environment. When running a test, the variables defined here will overwrite the ones defined in the .env file that is also in the project root.

The tests Directory

The tests directory is where we store all our test files. Feature tests should be in the tests/Feature directory, while unit tests should be in the tests/Unit directory. PHPUnit will automatically look for and execute the tests that are stored inside these directories as defined in the phpunit.xml file. The CreatesApplication.php and TestCase.php files bootstrap the application before running the tests. We don't need to change anything in these files.

Let's take a look at an example test. Open the ./tests/Unit/ExampleTest.php file in your text editor:

code ./tests/Unit/ExampleTest.php
Copied!
./tests/Unit/ExampleTest.php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_that_true_is_true()
    {
        $this->assertTrue(true);
    }
}
Copied!

This example test is quite basic as it tests if true is true. The assertTrue() method is expecting a statement that returns the boolean value true. This method is called an assertion, and PHPUnit provides us with many other useful assertions, which will be discussed later in this article. You can find a complete list of all PHPUnit assertions in their official documentation.

To run this test, open a new terminal instance (so that Laravel Sail keeps running) and execute the following command. Make sure you are at the root directory of your project.

./vendor/bin/phpunit
Copied!
Output
Time: 00:00.123, Memory: 20.00 MB

OK (2 tests, 2 assertions)

This output indicates that Laravel ran two tests and two assertions (there is another example test in the Feature directory, which includes another assertion), and both are successful.

We can deliberately cause the test to fail by changing the argument that is passed to the assertTrue() method as shown below:

./tests/Unit/ExampleTest.php
. . .
public function test_that_true_is_true()
{
$this->assertTrue("this is a string");
} . . .
Copied!

Now run the test again:

./vendor/bin/phpunit
Copied!
Output
Time: 00:00.141, Memory: 20.00 MB

There was 1 failure:

1) Tests\Unit\ExampleTest::test_that_true_is_true
Failed asserting that 'this is a string' is true.

/Users/erichu/Documents/GitHub/laravel-unit-test/code/tests/Unit/ExampleTest.php:16

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

When you undo the above changes, the test should pass again.

Next, let's try something more exciting by creating another test below the test_that_true_is_true() function:

./tests/Unit/ExampleTest.php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
    . . .

    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_that_name_is_jack()
    {
        $name = "John";
        // $name = "Jack";
        $this->assertTrue($name == "Jack");
    }
}
Copied!

Since the variable $name is tied with the value John, and John obviously does not equal Jack, this test will fail.

Run this test with the following command:

./vendor/bin/phpunit
Copied!
Output
Time: 00:00.008, Memory: 8.00 MB

There was 1 failure:

1) Tests\Unit\ExampleTest::test_that_name_is_jack
Failed asserting that false is true.

/Users/erichu/Documents/GitHub/laravel-unit-test/code/tests/Unit/ExampleTest.php:29

FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

Before you proceed with the next step, change $name back to 'Jack' so that you won't get failures in future test runs.

Step 3 — Creating a test from scratch

In this section, we will learn how to create our own tests from scratch. The code below constitutes what we'll be testing. It defines a room that contains multiple people. We can add a new person to the room, remove a person from the room, and check if a person is in the room.

Create a new app/Room.php file in your editor and paste the code below into the file:

code app/Room.php
Copied!
./app/Room.php
<?php

namespace App;

class Room
{
  /**
   * @var array
   */
  protected $people = [];

  /**
   * Constructor. Fill the room with the given people.
   *
   * @param array $people
   */
  public function __construct($people = [])
  {
    $this->people = $people;
  }

  /**
   * Check if the specified person is in the room.
   *
   * @param string $person
   * @return bool
   */
  public function has($person)
  {
    return in_array($person, $this->people);
  }

  /**
   * Add a new person to the room.
   *
   * @param string $person
   * @return array
   */
  public function add($person)
  {
    array_push($this->people, $person);
    return $this->people;
  }

  /**
   * Remove a person from the room.
   *
   * @param string $person
   * @return array
   */
  public function remove($person)
  {
    if (($key = array_search($person, $this->people)) !== false) {
      unset($this->people[$key]);
    }
    return $this->people;
  }
}
Copied!

Next, let's create a new unit test with the following command:

./vendor/bin/sail php artisan make:test RoomTest --unit
Copied!
Output
Test created successfully.

The above command creates a RoomTest.php file in the tests/Unit directory, and populates it with some boilerplate code:

./tests/Unit/RoomTest.php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class RoomTest extends TestCase
{
    /**
     * A basic unit test example.
     *
     * @return void
     */
    public function test_example()
    {
        $this->assertTrue(true);
    }
}
Copied!

The assertTrue() and assertFalse() assertions

Go ahead and open the tests/Unit/RoomTest.php file that was generated and update its contents as follows:

./tests/Unit/RoomTest.php
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

// Import the room
use App\Room;
class RoomTest extends TestCase { /** * Test the has() method in Room class * * @return void */ public function test_room_has() {
$room = new Room(["Jack", "Peter", "Amy"]); // Create a new room
$this->assertTrue($room->has("Jack")); // Expect true
$this->assertFalse($room->has("Eric")); // Expect false
} }
Copied!

We've already seen the assertTrue() method in step 2. The assertFalse() method works in the same manner except that it expects a false value. In this example, "Jack" is in the room, and "Eric" is not, so $room->has("Jack") returns true and $room->has("Eric") returns false, causing both assertions to pass.

You can try it out by running the command below. Notice that we can choose to run a specific test file by specifying the path to the file like this:

./vendor/bin/phpunit tests/Unit/RoomTest.php
Copied!
Output
Time: 00:00.012, Memory: 10.00 MB

OK (1 tests, 2 assertions)

The assertContains() assertion

Let's write another test to verify the behavior of the add() method. Instead of assertTrue() or assertFalse(), we'll go a different way. We will add a new person to the room, then check if the room contains that person using the assertContains() method.

Add the following function to the RoomTest class beneath the test_room_has() method:

./tests/Unit/RoomTest.php
. . .
/**
 * Test the add() method in Room class
 *
 * @return void
 */
public function test_room_add()
{
    $room = new Room(["Jack"]); // Create a new room
$this->assertContains("Peter", $room->add("Peter"));
}
Copied!

The assertContains() assertion takes two parameters: the first one is the value we expect the array, which is the second parameter, to contain. In our example, a new room that contains only Jack is created, and then $room->add("Peter") adds Peter to the room. Finally, we use assertContains() to test if Peter is in the room.

Run the test with the following command:

./vendor/bin/phpunit tests/Unit/RoomTest.php
Copied!

You should observe that the test passes:

Output
Time: 00:00.007, Memory: 8.00 MB

OK (2 tests, 3 assertions)

Laravel also offers a powerful command-line tool called artisan which we can use to execute our tests as well. The artisan command provides a more detailed output:

./vendor/bin/sail php artisan test
Copied!
Output

 PASS  Tests\Unit\ExampleTest
✓ that true is true
✓ that name is jack

 PASS  Tests\Unit\RoomTest
✓ room has
✓ room add

Tests:  4 passed
Time:   1.47s

With the help of the artisan command, it is possible for us to check how much of our code is covered by the tests. First, we need to install a PHP extension called Xdebug by following these instructions.

Once Xdebug is installed, we need to add a new environment variable in the .env file to set its coverage mode:

.env

APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:S07501pjvBOoBrOPMFMUg5J4Gyurq9FKDLWuIkwt5tk=
APP_DEBUG=true
APP_URL=http://app.test

. . .

SCOUT_DRIVER=meilisearch
MEILISEARCH_HOST=http://meilisearch:7700

SAIL_XDEBUG_MODE=develop,debug,coverage
Copied!

After saving the .env file, you need to stop and restart Laravel Sail by pressing Ctrl-C in the terminal and executing ./vendor/bin/sail up once more. Now, we can run the artisan test command with the --coverage flag to see how much of our code is covered by the tests:

./vendor/bin/sail php artisan test --coverage
Copied!
Output
   PASS  Tests\Unit\ExampleTest
  ✓ that true is true
  ✓ that name is jack

   PASS  Tests\Unit\RoomTest
  ✓ room has
  ✓ room add

  Tests:  4 passed
  Time:   1.17s

  Console/Kernel  ........................................... 100.0 %
  Exceptions/Handler  ....................................... 100.0 %
  Http/Controllers/Controller  .............................. 100.0 %
  Http/Kernel  .............................................. 100.0 %
  Http/Middleware/Authenticate ................................ 0.0 %
  Http/Middleware/EncryptCookies  ........................... 100.0 %
  Http/Middleware/PreventRequestsDuringMaintenance  ......... 100.0 %
  Http/Middleware/RedirectIfAuthenticated ..................... 0.0 %
  Http/Middleware/TrimStrings  .............................. 100.0 %
  Http/Middleware/TrustHosts .................................. 0.0 %
  Http/Middleware/TrustProxies  ............................. 100.0 %
  Http/Middleware/VerifyCsrfToken  .......................... 100.0 %
  Models/User  .............................................. 100.0 %
  Providers/AppServiceProvider  ............................. 100.0 %
  Providers/AuthServiceProvider  ............................ 100.0 %
  Providers/BroadcastServiceProvider .......................... 0.0 %
  Providers/EventServiceProvider  ........................... 100.0 %
  Providers/RouteServiceProvider 32..37, 49 .................. 33.3 %
  Room  ..................................................... 100.0 %

  Total Coverage ............................................. 48.4 %

As you can see, the code coverage percentage for each file is listed below the test results, and we have a 48.4% overall test coverage for our application.

The assertCount() assertion

Our final test for the Room class involves testing the remove() method with assertCount(). This method works similarly to assertContains(), except that the first parameter must be the number of items in the array.

Add the following function below the test_room_add() method:

./tests/Unit/RoomTest.php
/**
 * Test the add() method in Room class
 *
 * @return void
 */
public function test_room_remove()
{
    $room = new Room(["Jack", "Peter"]); // Create a new room
    $this->assertCount(1, $room->remove("Peter"));
}
Copied!

The function above creates a new room with two members and uses the assertCount() method to test whether removing a member from the room reduces its member count to 1.

Rerun the test again to view the results:

./vendor/bin/sail php artisan test ./tests/Unit/RoomTest.php
Copied!
Output
 PASS  Tests\Unit\RoomTest
✓ room has
✓ room add
✓ room remove

Tests:  3 passed
Time:   1.47s

Step 4 — Testing HTTP responses

In a real-world web application, we probably won't need to test simple arrays as in the previous step. Instead, we'll probably be dealing with APIs, HTTP requests, and responses. Laravel also provides an example of this type of test.

Go ahead and open the ./tests/Feature/ExampleTest.php file in your text editor:

code ./tests/Feature/ExampleTest.php
Copied!
./tests/Feature/ExampleTest.php
public function test_the_application_returns_a_successful_response()
{
    $response = $this->get('/');

    $response->assertStatus(200);
}
Copied!

In this example, a GET request is sent to the root URL, and the returned response is assigned to the $response variable. On the following line, we check if the response has a success code of 200 with the assertStatus() assertion.

Sometimes, an endpoint requires us to pass some query parameters. In such cases, we can do this:

public function test_the_application_returns_a_successful_response()
{
$response = $this->post('/user', ['name' => 'Amy']);
$response->assertStatus(200); }
Copied!

Just testing the response status is not enough for a real application. Usually, we need to test the response body to ensure the data that is returned to us is valid and matches expectations. In most cases, the response data is either in JSON or HTML format. In the next two sections, we will discuss how to test them both in detail.

Testing JSON data

The backend and frontend of most modern web applications are usually running separately, and data is shared between them in JSON format. Laravel offers us some helpers for testing that the JSON data received from an endpoint is valid and accurate. For instance, the assertJson() method is provided to ascertain the validity of JSON data.

Let's take a look at the following example. Open the routes/web.php file and create a new route. We'll make this route return some JSON data when a GET request is made to it.

code routes/web.php
Copied!
./routes/web.php
. . .
Route::get('/json-test', function () {
    return response()->json([
        'name' => 'Jone',
        'updated' => true,
    ]);
});
Copied!

Create a test for this route using the following command:

./vendor/bin/sail php artisan make:test JSONTest
Copied!
Output
Test created successfully.

Open the newly created ./tests/Feature/JSONTest.php and update its contents as follows:

./tests/Feature/JSONTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class JSONTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_json()
    {
        $response = $this->get('/json-test');

        $response->assertStatus(200);
$response->assertJson([
'updated' => true,
]);
} }
Copied!

In this test, we're making a GET request to the /json-test route and assigning its response to the $response variable. Next, we test if the response status is 200, and if the response data contains the value 'updated' => true.

If you run this test now, both assertions should pass:

./vendor/bin/sail php artisan test ./tests/Feature/JSONTest.php
Copied!
Output
   PASS  Tests\Feature\JSONTest
  ✓ json

  Tests:  1 passed
  Time:   0.69s

The test above only checks that the JSON response contains a property. In cases where you want the JSON data to be an exact match, you can use the assertExactJson() assertion like this:

. . .
public function test_json()
{
    $response = $this->get('/json-test');

    $response->assertStatus(200);
$response->assertExactJson([
'updated' => true,
]);
} . . .
Copied!

After making the above changes, the test will fail since the provided JSON is not an exact match with the response.

./vendor/bin/sail php artisan test ./tests/Feature/JSONTest.php
Copied!
Output
   FAIL  Tests\Feature\JSONTest
  ⨯ json

  ---

  • Tests\Feature\JSONTest > json
  Failed asserting that two strings are equal.

  at tests/Feature/JSONTest.php:25
     21▕         // $response->assertJson([
     22▕         //     'created' => true,
     23▕         // ]);
     24▕         $response->assertExactJson([
  ➜  25▕             'created' => true,
     26▕         ]);
     27▕     }
     28▕ }
  --- Expected
  +++ Actual
  @@ @@
   '{\n
  -    "created": true\n
  +    "created": true,\n
  +    "name": "Abigail"\n
   }'

  Tests:  1 failed
  Time:   0.76s

Testing HTML views

Sometimes, we expect a route to return some HTML instead of JSON. Laravel is also able to test HTML responses for us so that we can verify that the route is working correctly.

Before we write a new test, let's create the view and router we'll be testing. Head over to the ./resources/views directory and create a new file called test.blade.php.

code ./resources/views/test.blade.php
Copied!
./resources/views/test.blade.php
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <p>The name is {{ $name }}.</p>
</body>

</html>
Copied!

This view expects a $name variable, and this variable can be supplied from the router. Go the ./routes/web.php file and create the following route:

./routes/web.php
. . .
Route::get('/view-test', function () {
    return view('test', ['name' => 'Taylor']);
});
Copied!

Next, let's create a new test class for the view we just created, and we'll call it ViewTest.php:

./vendor/bin/sail php artisan make:test ViewTest
Copied!
Output
Test created successfully.

Open the ./tests/Feature/ViewTest.php file and create the following test:

code ./tests/Feature/ViewTest.php
Copied!
./tests/Feature/ViewTest.php
. . .
class ViewTest extends TestCase
{
    /**
     * Test if the returned HTML page contains the name Taylor.
     *
     * @return void
     */
    public function test_if_view_contains_Taylor()
    {
        $response = $this->get('/view-test');

        $response->assertStatus(200);
        $response->assertSee('Taylor');
        $response->assertSee('<p>The name is Taylor.</p>', false);
    }
}
Copied!

In this example, we first visit the URL /view-test, and assign the response to the variable $response. Then we use the assertStatus() assertion to verify that the response has a success code of 200.

On the following line, we used the assertSee() method to test if the returned HTML page contains the word Taylor. This assertion will automatically escape the given string unless you pass false as the second parameter, as seen in the third assertion. If allow string escaping, <p> will be treated as &lt;p&gt; and not as an HTML tag.

Run the test with the following command:

./vendor/bin/sail php artisan test ./tests/Feature/ViewTest.php
Copied!
Output
   PASS  Tests\Feature\ViewTest
  ✓ if view contains  taylor

  Tests:  1 passed
  Time:   0.72s

Similar assertions include assertSeeInOrder(), assertSeeText(), etc. You can find details about these assertions here.

Step 5 — Testing your database

So far, we've discussed how to write basic unit tests, and then we demonstrated some strategies for testing HTTP responses in real-world applications. Let's take it one step further by testing code that reads from or writes to a database.

We know that for most applications, we need to store information inside a database, so it is very important ensure that the database is working as designed before we deploy our application into production. To do this, the first step is generating some fake data inside our database.

Generating fake data

Laravel Sail already took care of the database configurations for us when we ran the ./vendor/bin/sail up command for the first time. By default, Laravel provides us with a User model (./app/Models/User.php) and the corresponding migration file (./database/migrations/2014_10_12_000000_create_users_table.php). In this section, we will use this User model to demonstrate how to test databases in Laravel.

Go ahead and run the migrations directly. Remember that we need to run the command within Laravel Sail:

./vendor/bin/sail php artisan migrate
Copied!
Output
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated:  2014_10_12_000000_create_users_table (56.59ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated:  2014_10_12_100000_create_password_resets_table (30.96ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated:  2019_08_19_000000_create_failed_jobs_table (32.18ms)
Migrating: 2019_12_14_000001_create_personal_access_tokens_table
Migrated:  2019_12_14_000001_create_personal_access_tokens_table (63.55ms)

Right now, the database is still empty, but we can use a factory to generate some data for testing. Here's the artisan command for generating a new factory:

./vendor/bin/sail php artisan make:factory PostFactory
Copied!
Output
Factory created successfully.

However, Laravel provides us with an example for the User model. Open the ./database/factories/UserFactory.php file in your text editor and examine its contents:

code ./database/factories/UserFactory.php
Copied!
./database/factories/UserFactory.php
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
 */
class UserFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition()
    {
        return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'remember_token' => Str::random(10),
]; } . . . }
Copied!

The factory file tells Laravel what type of data should fill the corresponding field. In this example, the name and email fields are generated using the PHP's Faker library, email_verified_at is set to the current time, password is a hard-coded string, and remember_token is generated randomly as a string containing 10 characters.

Running database tests

To use this factory to generate test data, we must ensure that it is tied to the User model. Open file ./app/Models/User.php and make sure the User model is using HasFactory:

./app/Models/User.php
<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { [hightlight] use HasApiTokens, HasFactory, Notifiable; . . . }
Copied!

Laravel will automatically go to ./database/factories and look for a class name matching the model name with a suffix Factory (<model_name>Factory.php).

Next, it is time for us to generate a test for the user database table. Run the following command in your terminal:

./vendor/bin/sail php artisan make:test DatabaseTest
Copied!
Output
Test created successfully.

Open ./tests/Feature/DatabaseTest.php in your text editor and create a new test for the user database:

code ./tests/Feature/DatabaseTest.php
Copied!
./tests/Feature/DatabaseTest.php
<?php
namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

use App\Models\User;

class DatabaseTest extends TestCase
{
    use RefreshDatabase;

    /**
     * Test the user database.
     *
     * @return void
     */
    public function test_user_database()
    {
        User::factory()->count(3)->create();

        $this->assertDatabaseCount('users', 3);
    }
}
Copied!

In this example, we used RefreshDatabase to make sure after each test, the database is reset to its original state. You can comment this line out if you don't think this is necessary. We must also import the User model into the test file.

Inside the test_user_database() function, we inserted three records into the users table through the user factory we just created. Then we used the assertDatabaseCount() assertion to verify that there are three records in the users table.

Of course, there are many more useful assertions available to us. For instance, assertDatabaseHas() test the existence of a specific record in the table and assertDatabaseMissing() verifies the lack of a record.

$this->assertDatabaseHas('users', [
    'email' => '[email protected]',
]);

$this->assertDatabaseMissing('users', [
    'email' => '[email protected]',
]);
Copied!

Finally, we can run the database test using the following command, and verify that it passes:

./vendor/bin/sail php artisan test tests/Feature/DatabaseTest.php
Copied!
Output
   PASS  Tests\Feature\DatabaseTest
  ✓ user database

  Tests:  1 passed
  Time:   1.44s

Step 6 — Testing in the browser with Laravel Dusk

Besides unit testing and feature testing, there is also browser testing which can be useful in certain situations. It allows us to simulate the user's behavior inside a browser, such as typing something in a form or pressing a button. To do this, we need to use a package called Laravel Dusk.

Installing Laravel Dusk

Go ahead and download the Laravel Dusk package using Composer:

./vendor/bin/sail composer require --dev laravel/dusk
Copied!
Output
Using version ^6.24 for laravel/dusk
./composer.json has been updated
Running composer update laravel/dusk
. . .
Discovered Package: laravel/dusk
Discovered Package: laravel/sail
Discovered Package: laravel/sanctum
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Discovered Package: spatie/laravel-ignition
. . .
Publishing complete.

Laravel Dusk is dependent on Google Chrome and the ChromeDriver, and if you followed this tutorial from the beginning and used Laravel Sail to initialize our project, the ChromeDriver should already be installed when we create the Docker container. You can verify this by opening the docker-compose.yml file in your project and check if the following code exists:

docker-compose.yml
. . .
selenium:
    image: 'selenium/standalone-chrome'
    volumes:
        - '/dev/shm:/dev/shm'
    networks:
        - sail
. . .
Copied!

If you are using an Apple Silicon Mac, you will need to use the seleniarm/standalone-chromium image instead:

docker-compose.yml
. . .
selenium:
image: 'seleniarm/standalone-chromium'
volumes: - '/dev/shm:/dev/shm' networks: - sail . . .
Copied!

This setup will ensure that all the necessary components are installed when you run the ./vendor/bin/sail up command. You do still need to install Google Chrome manually though, if you haven't done so already.

When you're ready to proceed, execute the browser test through the following artisan command. Make sure Laravel Sail is running in the background before doing so.

./vendor/bin/sail dusk
Copied!
Output
Time: 00:02.588, Memory: 22.00 MB

OK (1 test, 1 assertion)

If you didn't follow this tutorial from the start, and you are running the Laravel project without Docker, you need to install ChromeDriver with the duck:install command.

php artisan dusk:install
Copied!
Output
Dusk scaffolding installed successfully.
Downloading ChromeDriver binaries...
ChromeDriver binaries successfully installed for version 101.0.4951.41.

To run the browser test without Laravel Sail, use the following command, and the output will be the same as before:

php artisan dusk
Copied!

Writing browser tests with Laravel Dusk

You should find a Browser directory under the tests folder, and inside Browser, an ExampleTest.php file has been provided for us so let's take a closer look:

./tests/Browser/ExampleTest.php
<?php

namespace Tests\Browser;

use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;

class ExampleTest extends DuskTestCase
{
  public function testBasicExample()
    {
$this->browse(function (Browser $browser) {
$browser->visit('/')->assertSee('Laravel');
});
} }
Copied!

This file looks very similar to a unit or feature test, except that we are using the browse() method to simulate an actual browser. The root URL is visited and then the assertSee() method is used to check if the word Laravel exists in the resulting web page.

We can also do something more challenging with Laravel Dusk. In the following example, the /login route is visited and the form on the page is filled. Laravel will find the input field with the name attribute set to email and type in '[email protected]', and then do the same for the password field before finally pressing the Login button to submit the form.

$this->browse(function ($browser) use ($user) {
 $browser->visit('/login')
  ->type('email', '[email protected]')
  ->type('password', 'thisisapassword')
  ->press('Login');
});
Copied!

Laravel Dusk provides us with tons of other methods to interact with your web page but I can't list them all here. If you are interested in exploring further, consider reading their documentation pages.

Step 7 — Mocking external APIs in Laravel

In this section of the tutorial, we will discuss the concept of mocking and how to do it in a Laravel application. Sometimes, when we are testing our code, we wish to prevent a portion of the code from executing to keep the test fast and prevent side effects. For example, when we are testing code that interacts with an API, we generally don't want to make a request to the actual API to prevent the test from being flaky due to network conditions and service availability. We can mock that API response with the fake() method in such cases.

Let's start by creating a new test:

./vendor/bin/sail php artisan make:test MockTest
Copied!
Output
Test created successfully.

Next, add the test_mock_http() test to the MockTest.php file that we just created:

./tests/Feature/MockTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

use Illuminate\Support\Facades\Http;
class MockTest extends TestCase { /** * A basic feature test example. * * @return void */ public function test_mock_http() {
// This api is supposed to return a list of countries in JSON format,
// we can mock it so that it only returns Italy.
Http::fake([
'https://restcountries.com/v3.1/all' => Http::response(
[
'name' => 'Italy',
'code' => 'IT'
],
200
),
]);
$response = Http::get('https://restcountries.com/v3.1/all');
$this->assertJsonStringEqualsJsonString(
$response->body(),
json_encode([
'name' => 'Italy',
'code' => 'IT'
],)
);
} }
Copied!

Notice that we are using Laravel's Http facade to make HTTP requests here and this example API is supposed to return a list of countries in JSON format. In the above test, we are forcing it to return custom JSON data and a 200 response code, and we're using the assertJsonStringEqualsJsonString() method to verify if that is true.

Run this test with the following command:

./vendor/bin/sail php artisan test ./tests/Feature/MockTest.php
Copied!
Output
   PASS  Tests\Feature\MockTest
  ✓ mock http

  Tests:  1 passed
  Time:   0.86s

You can use this technique to test any piece of code that interacts with some API to avoid making network requests in your unit tests.

Step 8 — Testing your Laravel application with GitHub Actions

In the final section of the tutorial, I will demonstrate how to set up a Continuous Integration pipeline for testing your code with GitHub Actions. Ensure that you have Git installed on your system before proceeding.

First, open a new terminal and initialize a git repository in the root of your project through the following command:

git init -b main
Copied!
Output
Initialized empty Git repository in /<path_to_your_project_root_directory>/.git/

Commit all files in the directory to the local git repository:

git add . && git commit -m "initial commit"
Copied!
Output
[main 1e1c3e1] initial commit
 83 files changed, 11070 insertions(+)
 create mode 100644 .editorconfig
 create mode 100644 .env.example
 . . .

Next, we need to push this local repository to GitHub. Create a new repository for your project on GitHub:

New GitHub Repo

Copy the remote URL to this repository

github-repo-link

Head back to the terminal, and push your changes to the GitHub remote we just created using the following commands:

git remote add origin <remote_url>
Copied!
git branch -M main
Copied!
git push -u origin main
Copied!
Output
Enumerating objects: 108, done.
Counting objects: 100% (108/108), done.
Delta compression using up to 10 threads
Compressing objects: 100% (89/89), done.
Writing objects: 100% (107/107), 71.63 KiB | 6.51 MiB/s, done.
Total 107 (delta 7), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (7/7), done.
To https://github.com/ericnanhu/git-test.git
   eb84310..1e1c3e1  main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

Next, in order to create a CI pipeline, we need to create a .github/workflows directory under the root directory of our project. GitHub Actions will automatically look for workflow files in this directory.

mkdir -p .github/workflows
Copied!

Create a test.yml file inside the .github/workflows directory and open it up in your text editor:

code ./github/workflows/test.yml
Copied!

Paste the following code into the file:

./.github/workflows/test.yml
name: Laravel Test

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  laravel-tests:

    runs-on: ubuntu-latest

    steps:
    - uses: shivammathur/[email protected]
      with:
        php-version: '8.1'
    - uses: actions/[email protected]
    - name: Copy .env
      run: php -r "file_exists('.env') || copy('.env.example', '.env');"
    - name: Composer Update
      run: composer update
    - name: Install Dependencies
      run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
    - name: Generate key
      run: php artisan key:generate
    - name: Directory Permissions
      run: chmod -R 777 storage bootstrap/cache
    - name: Create Database
      run: |
        mkdir -p database
        touch database/database.sqlite
    - name: Execute tests (Unit and Feature tests) via PHPUnit
      env:
        DB_CONNECTION: sqlite
        DB_DATABASE: database/database.sqlite
      run: vendor/bin/phpunit
Copied!

This yml file essentially defines a series of commands that runs every time we push or make a pull request to the main branch. Once such an event is detected, the commands listed in the jobs.laravel-tests.steps section will be executed in order by GitHub. The runs-on property specifies that the testing environment will be the latest version of Ubuntu, and all the commands within steps have a name and a run property that defines the command to run.

The first step installs PHP 8.1 in the testing environment and the second checks out the GitHub repo so that the workflow can access it. Next, the .env.example file is copied into the .env file if it doesn't exist already, and composer is updated and used to install the necessary dependencies. The php artisan key:generate command is used to set the APP_KEY value in your .env file, and the necessary directory and database configurations are performed in the next two steps. Finally, the tests are executed through phpunit.

Go ahead and make some changes to your project, then commit and push them to the main branch, it will automatically trigger this pipeline. If nothing goes wrong, all jobs should be successful. You can see the result by visiting the Actions tab of your GitHub repository.

github-actions

Conclusion

Testing your code is a non-negotiable step in the software development life cycle. When you are creating a Laravel application, it is best to write feature/unit tests for every controller, database tests for every database table, and browser tests for every web page. This process guarantees the integrity of your code.

In this tutorial, we've talked about the basics of unit testing in Laravel. We've studied how to set up different testing environments in the PHPUnit configuration file phpunit.xml, how to create and run tests using terminal commands, and how to test code through assertions.

We also discussed how to test HTTP responses which is more practical for real-world web applications, and we talked about database testing and how to generate fake data for testing purposes. Finally, we briefly introduced browser testing which simulates real users in a real browser, and the concept of mocking, which allows us to override the functionality of a piece of code so that we could test other parts of our application.

Please note that this tutorial does not include everything on testing in Laravel, it merely opens the door for you to get started and learn more. You can read more about testing in Laravel's official documentation.

Check Uptime, Ping, Ports, SSL and more.
Get Slack, SMS and phone incident alerts.
Easy on-call duty scheduling.
Create free status page on your domain.
Got an article suggestion? Let us know
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.