Testing in Laravel
Laravel is built with testing in mind and includes support for testing with PHPUnit out of the box. Testing ensures your application works as expected and helps prevent bugs when making changes. In this lesson, we'll explore Laravel's powerful testing features.
Why Testing Matters
Testing provides several critical benefits:
- Confidence: Make changes knowing you won't break existing functionality
- Documentation: Tests serve as living documentation of how your code works
- Refactoring: Safely improve code structure without fear
- Bug Prevention: Catch issues before they reach production
- Better Design: Writing testable code leads to better architecture
Note: Laravel comes pre-configured with PHPUnit and includes a phpunit.xml file in your project root. Tests are stored in the tests/ directory.
Types of Tests in Laravel
Laravel supports two main types of tests:
1. Feature Tests
Feature tests (also called integration tests) test larger portions of your application, including how multiple classes interact with each other and even make HTTP requests to your application:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class UserRegistrationTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_register()
{
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'john@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertRedirect('/dashboard');
$this->assertDatabaseHas('users', [
'email' => 'john@example.com',
]);
}
public function test_registration_requires_valid_email()
{
$response = $this->post('/register', [
'name' => 'John Doe',
'email' => 'invalid-email',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
$response->assertSessionHasErrors('email');
}
}
2. Unit Tests
Unit tests focus on testing small, isolated portions of your code, typically individual methods or classes:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
use App\Services\PriceCalculator;
class PriceCalculatorTest extends TestCase
{
public function test_calculates_price_with_tax()
{
$calculator = new PriceCalculator();
$price = $calculator->calculateWithTax(100, 0.15);
$this->assertEquals(115, $price);
}
public function test_applies_discount_correctly()
{
$calculator = new PriceCalculator();
$price = $calculator->applyDiscount(100, 10);
$this->assertEquals(90, $price);
}
public function test_throws_exception_for_negative_price()
{
$this->expectException(\InvalidArgumentException::class);
$calculator = new PriceCalculator();
$calculator->calculateWithTax(-100, 0.15);
}
}
Creating Tests
Use Artisan commands to generate test files:
# Create a feature test
php artisan make:test UserProfileTest
# Create a unit test
php artisan make:test CalculatorTest --unit
# Create a test with specific methods
php artisan make:test PostTest --pest # For Pest testing framework
Running Tests
Execute your tests using PHPUnit or Artisan:
# Run all tests
php artisan test
# Run a specific test file
php artisan test tests/Feature/UserTest.php
# Run a specific test method
php artisan test --filter test_user_can_login
# Run tests in parallel (faster)
php artisan test --parallel
# Run with coverage report
php artisan test --coverage
Database Testing
Laravel provides several traits and methods for database testing:
RefreshDatabase Trait
This trait migrates and resets your database after each test:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class PostTest extends TestCase
{
use RefreshDatabase;
public function test_user_can_create_post()
{
$user = User::factory()->create();
$this->actingAs($user)
->post('/posts', [
'title' => 'My First Post',
'content' => 'This is the content.',
]);
$this->assertDatabaseHas('posts', [
'title' => 'My First Post',
'user_id' => $user->id,
]);
}
public function test_post_belongs_to_user()
{
$user = User::factory()->create();
$post = Post::factory()->create(['user_id' => $user->id]);
$this->assertInstanceOf(User::class, $post->user);
$this->assertEquals($user->id, $post->user->id);
}
}
Tip: Use the RefreshDatabase trait instead of DatabaseMigrations for faster tests. It uses transactions when possible, only running migrations when necessary.
HTTP Testing
Laravel provides a fluent API for testing HTTP requests and responses:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ApiTest extends TestCase
{
use RefreshDatabase;
public function test_api_returns_users_list()
{
User::factory()->count(3)->create();
$response = $this->getJson('/api/users');
$response->assertStatus(200)
->assertJsonCount(3, 'data')
->assertJsonStructure([
'data' => [
'*' => ['id', 'name', 'email', 'created_at']
]
]);
}
public function test_api_requires_authentication()
{
$response = $this->getJson('/api/profile');
$response->assertUnauthorized();
}
public function test_authenticated_user_can_access_profile()
{
$user = User::factory()->create();
$response = $this->actingAs($user)
->getJson('/api/profile');
$response->assertOk()
->assertJson([
'email' => $user->email,
]);
}
}
Common Assertions
Laravel provides many assertion methods for testing:
<?php
// Response Assertions
$response->assertOk(); // 200 status
$response->assertCreated(); // 201 status
$response->assertNotFound(); // 404 status
$response->assertForbidden(); // 403 status
$response->assertUnauthorized(); // 401 status
$response->assertRedirect('/home'); // Redirect assertion
// JSON Assertions
$response->assertJson(['key' => 'value']);
$response->assertJsonFragment(['name' => 'John']);
$response->assertJsonMissing(['secret' => 'hidden']);
$response->assertJsonPath('data.0.id', 1);
// Database Assertions
$this->assertDatabaseHas('users', ['email' => 'test@example.com']);
$this->assertDatabaseMissing('users', ['email' => 'deleted@example.com']);
$this->assertDatabaseCount('posts', 5);
$this->assertSoftDeleted('posts', ['id' => 1]);
// Authentication Assertions
$this->assertAuthenticated();
$this->assertGuest();
$this->assertAuthenticatedAs($user);
// Session Assertions
$response->assertSessionHas('key', 'value');
$response->assertSessionHasErrors(['email']);
$response->assertSessionMissing('key');
// View Assertions
$response->assertViewIs('posts.index');
$response->assertViewHas('posts');
$response->assertViewHasAll(['posts', 'categories']);
Mocking and Faking
Laravel provides convenient ways to mock external services and dependencies:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Notification;
use App\Mail\WelcomeEmail;
use App\Jobs\ProcessOrder;
use App\Models\User;
class MockingTest extends TestCase
{
public function test_welcome_email_is_sent()
{
Mail::fake();
$user = User::factory()->create();
// Trigger code that sends email
$user->sendWelcomeEmail();
Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
return $mail->hasTo($user->email);
});
}
public function test_job_is_dispatched()
{
Queue::fake();
// Trigger code that dispatches job
ProcessOrder::dispatch($orderId = 123);
Queue::assertPushed(ProcessOrder::class, function ($job) use ($orderId) {
return $job->orderId === $orderId;
});
}
public function test_file_upload_stores_file()
{
Storage::fake('public');
$response = $this->post('/upload', [
'file' => UploadedFile::fake()->image('photo.jpg')
]);
Storage::disk('public')->assertExists('photos/photo.jpg');
}
}
Warning: When using fakes, the actual code (sending emails, dispatching jobs, etc.) won't execute. Use fakes for testing that the code is called correctly, not for testing the implementation itself.
Test Databases
Configure a separate database for testing in your phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit>
<php>
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="sqlite"/>
<env name="DB_DATABASE" value=":memory:"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
</php>
</phpunit>
Exercise 1: Feature Test
Create a feature test for a blog post creation:
- Run:
php artisan make:test PostCreationTest
- Test that authenticated users can create posts
- Test that guests are redirected to login
- Test that validation works (title is required)
- Test that the post appears in the database
Exercise 2: Unit Test
Create a unit test for a discount calculator:
- Create a
DiscountCalculator class with a calculate() method
- Run:
php artisan make:test DiscountCalculatorTest --unit
- Test percentage discounts (10% off $100 = $90)
- Test fixed amount discounts ($10 off $100 = $90)
- Test that discounts can't exceed the original price
Exercise 3: HTTP Testing
Create a comprehensive API test:
- Test GET /api/products returns a list of products
- Test POST /api/products creates a new product (authenticated)
- Test PUT /api/products/{id} updates a product
- Test DELETE /api/products/{id} deletes a product
- Test that all endpoints require authentication
- Use factories to create test data
Best Practices
- Test One Thing: Each test method should verify one specific behavior
- Use Descriptive Names: Test method names should describe what they test
- Arrange-Act-Assert: Structure tests in three phases: setup, execute, verify
- Use Factories: Generate test data with factories instead of manual creation
- Fast Tests: Keep tests fast by using in-memory databases and mocking external services
- Independent Tests: Each test should be able to run independently
- Don't Test Framework: Focus on testing your application code, not Laravel itself
Summary
In this lesson, you learned:
- The difference between feature tests and unit tests
- How to create and run tests in Laravel
- Database testing with the RefreshDatabase trait
- HTTP testing and response assertions
- How to mock external services using fakes
- Common assertion methods for various scenarios
- Best practices for writing maintainable tests
Testing is an essential skill for professional Laravel development. Start by testing critical features, then gradually increase coverage. Well-tested code leads to more reliable, maintainable applications.