ホーム > Laravel > How to Test Using PHPUnit in Laravel 11
Laravel

How to Test Using PHPUnit in Laravel 11

Thank you for your continued support.
This article contains advertisements that help fund our operations.

This article provides a guide on testing in Laravel 11 using PHPUnit.

Introduction

Writing tests with PHPUnit is something anyone can do. It’s not as hard as it may seem, so instead of thinking, “Writing test code sounds tough, so maybe I’ll skip it,” give it a try by running a simple test from this article.

I remember feeling confident, realizing, “I can write tests!” just after running my first one. Writing a perfect test suite is incredibly challenging, like trying to be as skilled as Kevin De Bruyne in football.

Before Running PHPUnit

Check the Test Configuration File

Check the contents of the PHPUnit configuration file located at /phpunit.xml.

Use a Separate Test Database

Using a separate test database prevents interference with the development database. It’s tedious to reset development test data, so we adjust the configuration to connect to a test database instead.

You’ll see the following commented-out lines:


<!-- Before -->
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
<!-- <env name="DB_DATABASE" value=":memory:"/> -->

Change them to:


<!-- After -->
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>

Running Tests

Execute with a Command

Run the following command:

php artisan test

Alternatively:

./vendor/bin/phpunit

The first command works in newer versions of Laravel, while the second is for older versions.

Viewing Test Results

php artisan test
PASS Tests\Unit\ExampleTest
✓ that true is true
Tests: 0 skipped, 1 passed (1 assertions)
Duration: 0.66s

If you see output like this without any red text, you’re good to go. Congratulations!

Things to Know When Writing Test Code

For beginners, I’ll focus on essential points. Writing test code verifies the functionality of your implementation and ensures it doesn’t unintentionally break existing features. As features grow, manually checking everything becomes harder, so we use test automation.

Types of Test Code

  1. Functional (Feature) Tests
  2. Unit Tests

For PHPUnit in Laravel, these are the two main types to be familiar with: Feature tests and Unit tests.

What is a Feature Test?

A feature test tests functionality. In Laravel, this often means testing routing.

In the example below, we check if the route for / returns a 200 status.

public function test_a_basic_request()
{
$response = $this->get('/');
$response->assertStatus(200);
}

What is a Unit Test?

A unit test is a more granular test focused on individual functions. It’s designed to test function output without relying on other components.

Create a Calculator class with a simple addition function:


<?php

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

Now let’s test this addition function.

CalculatorTest.php

<?php

use PHPUnit\Framework\TestCase;

class CalculatorTest extends TestCase
{
    public function testAdd()
    {
        // Create an instance of the Calculator class
        $calculator = new Calculator();

        // Verify the method's results
        $this->assertEquals(4, $calculator->add(2, 2)); // 2 + 2 = 4
        $this->assertEquals(0, $calculator->add(-2, 2)); // -2 + 2 = 0
        $this->assertEquals(5, $calculator->add(3, 2)); // 3 + 2 = 5
    }
}

The assertEquals function checks if the result matches the expected value (4). If not, it raises an error; if it does, it counts as a passed assertion.

Writing a Feature Test

Testing Access to the Home Page

Now, let’s write a test to access /.

Creating a Test File via Command

Create a test file with the following command:

php artisan make:test HomeControllerTest

Ensure the file name ends with Test, like HomeControllerTest, so it’s recognized as a test file.

This command generates /tests/Feature/HomeControllerTest.php. Now, let’s write the test contents.

Writing in /tests/Feature/HomeControllerTest.php

The function name should start with test, such as test_example or testExample.

<?php

namespace Tests\Feature;

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

class HomeControllerTest extends TestCase
{
    /**
     * A basic feature test example.
     */
    public function test_example(): void
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}

Executing the Test Command

Run this file with:

php artisan test --filter=HomeControllerTest

The filter option lets you run specific tests.

   PASS  Tests\Feature\HomeControllerTest
  ✓ example                                                                                            0.81s

  Tests:    1 passed (1 assertions)
  Duration: 1.15s

In this test, we:

  1. Access /
  2. Assert the response status is 200.

Since we only assert once, the result shows 1 assertion.

You can view a list of available assertions at the following URL:

11.x HTTP Testing - Laravel #Available Assertions

Add Specifications and Test

Suppose that the homepage displays a list of posts.

public function index()
{
    $posts = Post::latest()->take(20)->get();
    return view('home',compact('posts'));
}

Retrieves the latest 20 records from the Post model

Let's test this.

Create Test Data with the Factory Class

First, prepare a Factory Class to insert test data.

Create test data for the Post Model.

php artisan make:factory PostFactory --model=Post

Write it in database/factories/PostFactory.php.

public function definition(): array
{
    return [
        'title' => 'Title',
        'content' => 'Content'
    ];
}

For more details on using Factory with faker, refer to the following article:

How to Create Test Data with Factory in Laravel

With the Factory class in place, you can now use the following test code:

Post::factory()->count(25)->create();

Rewrite /tests/Feature/HomControllerTest.php

Now, let's write a test in /tests/Feature/HomControllerTest.php to verify that the latest 20 records from the Post model are being retrieved.

use App\Models\Post;

// ...

public function test_home()
{
    // Create 25 posts as test data
    Post::factory()->count(25)->create();
    // Access `/` with a GET request
    $response = $this->get('/');
    // Verify that the status code is 200
    $response->assertStatus(200);
    // Check the content of `$posts` received by the view
    $response->assertViewHas('posts', function ($posts) {
        return $posts->count() === 20; // Confirm there are 20 posts
    });
    // Confirm `$posts` contains the latest 20 posts
    $latestPosts = Post::latest()->take(20)->get();
    $response->assertViewHas('posts', $latestPosts);
}

You can write multiple assert statements in a single function.

When this is executed:

PASS Tests\Feature\HomeControllerTest
✓ example 0.48s
Tests: 1 passed (24 assertions)
Duration: 0.65s

It was successful.

Next, let’s make an intentional mistake.

    $latestPosts = Post::latest()->take(19)->get();
    $response->assertViewHas('posts', $latestPosts);

Failed asserting that actual size 20 matches expected size 19.

This error appeared.

Are you starting to get the hang of it?

Writing Unit Tests

It gets harder here.

Function to Test

class UserManager
{
    protected $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    public function registerUser(array $userData): bool
    {
        // Check if the user already exists
        if ($this->userRepository->exists($userData['email'])) {
            throw new RuntimeException('This email address is already in use.');
        }

        // Register the user
        $user = $this->userRepository->create($userData);

        return true;
    }
}

Test Code

use PHPUnit\Framework\TestCase;

class UserManagerTest extends TestCase
{
    private $userRepository;

    protected function setUp(): void
    {
        // Create a Mock
        $this->userRepository = $this->createMock(UserRepository::class);
        $this->userManager = new UserManager($this->userRepository);
    }

    public function testRegisterUserSuccessfully()
    {
        // Dummy data
        $userData = [
            'email' => 'test@example.com',
            'password' => 'password123',
        ];

        // Set Mock behavior
        $this->userRepository->expects($this->once())
            ->method('exists')
            ->with($this->equalTo($userData['email']))
            ->willReturn(false);

        $this->userRepository->expects($this->once())
            ->method('create')
            ->with($this->equalTo($userData))
            ->willReturn((object) $userData);

        // Method call and assertion
        $result = $this->userManager->registerUser($userData);
        $this->assertTrue($result);
    }

    public function testRegisterUserWithExistingEmail()
    {
        $userData = [
            'email' => 'existing@example.com',
            'password' => 'password123',
        ];

        // Behavior indicating existing user
        $this->userRepository->expects($this->once())
            ->method('exists')
            ->with($this->equalTo($userData['email']))
            ->willReturn(true);

        // Check if exception is thrown
        $this->expectException(RuntimeException::class);
        $this->expectExceptionMessage('This email address is already in use.');

        $this->userManager->registerUser($userData);
    }
}

Although it’s quite complex, we are testing the function registerUser().

  1. Display an error message if the email address is duplicated
  2. Register the user if there is no issue

It may be challenging, so it’s a good idea to try this once you have a deeper understanding of classes and functions.

Useful Concepts to Deepen Your Understanding

  1. Creating test data with Factory
  2. Types of assert
  3. setUp()
  4. tearDown()
  5. Mock

There might be more, but these are what came to mind.

You can search for terms to explore more on these topics.

That's all.

I hope this helps someone who’s trying to write better tests.

Good luck!

Please Provide Feedback
We would appreciate your feedback on this article. Feel free to leave a comment on any relevant YouTube video or reach out through the contact form. Thank you!