Laravel Framework

Testing in Laravel

20 min Lesson 26 of 45

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:

  1. Run: php artisan make:test PostCreationTest
  2. Test that authenticated users can create posts
  3. Test that guests are redirected to login
  4. Test that validation works (title is required)
  5. Test that the post appears in the database

Exercise 2: Unit Test

Create a unit test for a discount calculator:

  1. Create a DiscountCalculator class with a calculate() method
  2. Run: php artisan make:test DiscountCalculatorTest --unit
  3. Test percentage discounts (10% off $100 = $90)
  4. Test fixed amount discounts ($10 off $100 = $90)
  5. Test that discounts can't exceed the original price

Exercise 3: HTTP Testing

Create a comprehensive API test:

  1. Test GET /api/products returns a list of products
  2. Test POST /api/products creates a new product (authenticated)
  3. Test PUT /api/products/{id} updates a product
  4. Test DELETE /api/products/{id} deletes a product
  5. Test that all endpoints require authentication
  6. 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.