Introduction
Comprehensive Testing in Laravel with PHPUnit
The Complete Guide to Unit Testing in Laravel Applications
Unit testing is an essential practice in modern web development that ensures your Laravel applications remain robust, maintainable, and bug-free. PHPUnit is Laravel’s built-in testing framework that provides a powerful suite of tools for writing comprehensive tests. In this guide, we’ll explore every aspect of testing in Laravel, from basic test structure to advanced testing techniques and best practices.
Getting Started with PHPUnit in Laravel
Laravel provides several testing types that help you verify different aspects of your application
Basic Test Structure
Every Laravel application comes with PHPUnit pre-configured. Tests are located in the tests/ directory and follow the PSR-4 autoloading standard.
ExampleTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
use RefreshDatabase;
/**
* Basic test example
*/
public function test_basic_test()
{
// Assert that true is true
$this->assertTrue(true);
}
/**
* Test user creation
*/
public function test_user_creation()
{
$user = User::factory()->create();
$this->assertInstanceOf(User::class, $user);
$this->assertDatabaseHas('users', [
'email' => $user->email,
]);
}
}
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>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">app</directory>
</include>
</coverage>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
</php>
</phpunit>
Feature Tests
Feature tests allow you to test larger portions of your code, including how several objects interact with each other, or even a full HTTP request to a JSON endpoint.
AuthTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class AuthTest extends TestCase
{
use RefreshDatabase;
/**
* Test user can view login page
*/
public function test_user_can_view_login_page()
{
$response = $this->get('/login');
$response->assertStatus(200);
$response->assertViewIs('auth.login');
}
/**
* Test user can login with correct credentials
*/
public function test_user_can_login_with_correct_credentials()
{
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password123',
]);
$response->assertRedirect('/home');
$this->assertAuthenticatedAs($user);
}
/**
* Test user cannot login with incorrect password
*/
public function test_user_cannot_login_with_incorrect_password()
{
$user = User::factory()->create();
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'wrong-password',
]);
$response->assertSessionHasErrors('email');
$this->assertGuest();
}
}
Testing Controllers
Controller tests verify that your controllers handle requests properly and return the expected responses.
UserControllerTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
/**
* Test index returns all users
*/
public function test_can_get_all_users()
{
User::factory()->count(3)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonCount(3, 'data')
->assertJsonStructure([
'data' => [
* => [
'id',
'name',
'email',
'created_at',
'updated_at'
]
]
]);
}
/**
* Test can create a new user
*/
public function test_can_create_user()
{
$userData = [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
];
$response = $this->postJson('/api/users', $userData);
$response->assertStatus(201)
->assertJson([
'data' => [
'name' => 'John Doe',
'email' => 'john@example.com'
]
]);
$this->assertDatabaseHas('users', [
'email' => 'john@example.com'
]);
}
/**
* Test validation rules for user creation
*/
public function test_user_creation_requires_valid_data()
{
$response = $this->postJson('/api/users', []);
$response->assertStatus(422)
->assertJsonValidationErrors([
'name',
'email',
'password'
]);
}
}
Advanced Testing Techniques
Database Testing
Laravel provides several helpers for testing database interactions, including model factories, database assertions, and transactions.
DatabaseTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
class DatabaseTest extends TestCase
{
use RefreshDatabase;
/**
* Test database transactions
*/
public function test_database_transactions()
{
// Start a database transaction
$this->beginDatabaseTransaction();
$user = User::factory()->create();
$this->assertDatabaseHas('users', [
'id' => $user->id,
'email' => $user->email
]);
// Rollback transaction after test
$this->rollbackDatabaseTransaction();
// User should no longer exist after rollback
$this->assertDatabaseMissing('users', [
'id' => $user->id
]);
}
/**
* Test model relationships
*/
public function test_user_has_posts()
{
$user = User::factory()->create();
$post = Post::factory()->create([
'user_id' => $user->id
]);
$this->assertTrue($user->posts->contains($post));
$this->assertEquals(1, $user->posts->count());
}
/**
* Test soft deletes
*/
public function test_soft_delete_user()
{
$user = User::factory()->create();
$user->delete();
$this->assertSoftDeleted('users', [
'id' => $user->id
]);
// User should still exist in database with deleted_at timestamp
$this->assertDatabaseHas('users', [
'id' => $user->id
]);
}
/**
* Test database seeding
*/
public function test_database_seeding()
{
$this->seed(); // Run all seeders
$this->assertDatabaseCount('users', 10);
$this->assertDatabaseCount('posts', 50);
// Or seed specific seeder
$this->seed('UsersTableSeeder');
}
}
Mocking & Stubbing
Mocking allows you to replace complex dependencies with simpler, controlled implementations during testing.
MockingTest.php
<?php
namespace Tests\Unit;
use Tests\TestCase;
use App\Services\PaymentGateway;
use App\Services\NotificationService;
use Mockery;
use Mockery\MockInterface;
class MockingTest extends TestCase
{
/**
* Test mocking a service
*/
public function test_mocking_payment_gateway()
{
// Create a mock of PaymentGateway
$mock = $this->mock(PaymentGateway::class, function (MockInterface $mock) {
// Mock the charge method to return true
$mock->shouldReceive('charge')
->once()
->with(100, 'valid_token')
->andReturn(true);
});
// Inject the mock and test
$result = $mock->charge(100, 'valid_token');
$this->assertTrue($result);
}
/**
* Test partial mocking
*/
public function test_partial_mocking()
{
// Create a partial mock - only mock specific methods
$mock = $this->partialMock(NotificationService::class);
$mock->shouldReceive('sendEmail')
->once()
->with('user@example.com', 'Welcome!')
->andReturn(true);
// Other methods will work normally
$result = $mock->sendEmail('user@example.com', 'Welcome!');
$this->assertTrue($result);
}
/**
* Test mocking facades
*/
public function test_mocking_facades()
{
Cache::shouldReceive('get')
->once()
->with('key')
->andReturn('cached_value');
$value = Cache::get('key');
$this->assertEquals('cached_value', $value);
}
/**
* Test event mocking
*/
public function test_event_mocking()
{
Event::fake();
// Perform action that should fire event
$user = User::factory()->create();
// Assert event was dispatched
Event::assertDispatched(UserRegistered::class, function ($event) use ($user) {
return $event->user->id === $user->id;
});
// Assert event was not dispatched
Event::assertNotDispatched(UserDeleted::class);
}
}
Testing APIs
API testing is crucial for modern web applications. Laravel provides excellent tools for testing JSON APIs.
ApiTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Laravel\Sanctum\Sanctum;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ApiTest extends TestCase
{
use RefreshDatabase;
/**
* Test API authentication with Sanctum
*/
public function test_authenticated_api_request()
{
$user = User::factory()->create();
// Authenticate user using Sanctum
Sanctum::actingAs($user);
$response = $this->getJson('/api/user');
$response->assertStatus(200)
->assertJson([
'id' => $user->id,
'name' => $user->name,
'email' => $user->email
]);
}
/**
* Test API pagination
*/
public function test_api_pagination()
{
User::factory()->count(30)->create();
$response = $this->getJson('/api/users?page=2');
$response->assertStatus(200)
->assertJsonStructure([
'data',
'links',
'meta' => [
'current_page',
'last_page',
'per_page',
'total'
]
]);
$response->assertJson([
'meta' => [
'current_page' => 2
]
]);
}
/**
* Test API rate limiting
*/
public function test_api_rate_limiting()
{
$user = User::factory()->create();
Sanctum::actingAs($user);
// Make multiple requests to trigger rate limit
for ($i = 0; $i < 60; $i++) {
$response = $this->getJson('/api/user');
}
// 61st request should be rate limited
$response = $this->getJson('/api/user');
$response->assertStatus(429); // Too Many Requests
}
}
Conclusion
Mastering PHPUnit testing in Laravel transforms how you build and maintain robust applications.
By understanding different testing types, leveraging Laravel’s testing helpers, and utilizing advanced techniques like mocking and API testing, you can build reliable, maintainable applications with comprehensive test coverage.
Key Takeaways:
- Always write tests alongside your code implementation
- Use database transactions to keep tests isolated
- Leverage model factories for consistent test data
- Mock external dependencies to make tests predictable
- Test both success and failure scenarios
- Monitor test performance and optimize slow tests
Remember that comprehensive testing is not just about finding bugs—it’s about creating a safety net that allows you to refactor with confidence, document behavior, and ensure your application works as expected. Start with simple tests and gradually incorporate more advanced techniques as your application grows.
PHPUnit is the default testing framework that comes with Laravel. It provides a powerful suite of tools for writing unit tests, feature tests, and integration tests to ensure your application works correctly.

Leave a Reply