How to Test Using PHPUnit in Laravel 11
Thank you for your continued support.
This article contains advertisements that help fund our operations.
Table Of Contents
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
- Functional (Feature) Tests
- 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:
- Access
/
- 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()
.
- Display an error message if the email address is duplicated
- 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
- Creating test data with Factory
- Types of assert
- setUp()
- tearDown()
- 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!