Laravel Testing Strategies: Unit, Feature, and Integration Tests

Master Laravel testing with comprehensive strategies for unit tests, feature tests, and integration tests for businesses in Belfast, Dublin, and across the Island of Ireland. Learn best practices, testing tools, and how to achieve high test coverage in Laravel applications.

Testing is a critical aspect of professional Laravel development. Well-tested applications are more reliable, easier to maintain, and less prone to bugs. Laravel provides excellent testing tools and features that make writing tests straightforward and effective.

In this comprehensive guide, we'll explore Laravel testing strategies covering unit tests, feature tests, and integration tests. You'll learn how to write effective tests, use Laravel's testing features, and implement best practices for maintaining high-quality code.

Why Testing Matters in Laravel

Testing provides numerous benefits for Laravel applications:

  • Bug Prevention: Catch errors before they reach production
  • Code Confidence: Refactor and update code with confidence
  • Documentation: Tests serve as living documentation
  • Regression Prevention: Ensure new changes don't break existing functionality
  • Faster Development: Automated tests save time in the long run

Laravel Testing Tools

PHPUnit Integration

Laravel uses PHPUnit as its testing framework, which comes pre-configured:

// phpunit.xml is already configured
// Run all tests
php artisan test

// Run specific test file
php artisan test tests/Feature/ExampleTest.php

// Run with coverage
php artisan test --coverage

Laravel's Testing Features

Laravel provides powerful testing helpers:

  • Database Testing: Automatic database transactions
  • HTTP Testing: Simulate HTTP requests
  • Authentication Testing: Test authenticated users
  • Queue Testing: Test queued jobs
  • Mail Testing: Assert emails are sent

Types of Tests

Unit Tests

Unit tests focus on testing individual components in isolation:

// tests/Unit/UserServiceTest.php
namespace Tests\Unit;

use Tests\TestCase;
use App\Services\UserService;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserServiceTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_create_user()
    {
        $service = new UserService();
        $user = $service->createUser([
            'name' => 'John Doe',
            'email' => 'john@example.com',
        ]);

        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user->name);
        $this->assertEquals('john@example.com', $user->email);
    }
}

Feature Tests

Feature tests test complete features from the user's perspective:

// tests/Feature/UserRegistrationTest.php
namespace Tests\Feature;

use Tests\TestCase;
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('/home');
        $this->assertDatabaseHas('users', [
            'email' => 'john@example.com',
        ]);
    }
}

Integration Tests

Integration tests verify that multiple components work together:

// tests/Feature/OrderProcessingTest.php
namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use App\Models\Product;
use Illuminate\Foundation\Testing\RefreshDatabase;

class OrderProcessingTest extends TestCase
{
    use RefreshDatabase;

    public function test_complete_order_flow()
    {
        $user = User::factory()->create();
        $product = Product::factory()->create();

        // Login
        $this->actingAs($user);

        // Add to cart
        $this->post('/cart/add', [
            'product_id' => $product->id,
            'quantity' => 2,
        ]);

        // Checkout
        $response = $this->post('/checkout', [
            'payment_method' => 'stripe',
        ]);

        $response->assertRedirect('/orders');
        $this->assertDatabaseHas('orders', [
            'user_id' => $user->id,
        ]);
    }
}

Writing Unit Tests

Testing Models and Services

// tests/Unit/UserTest.php
namespace Tests\Unit;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class UserTest extends TestCase
{
    use RefreshDatabase;

    public function test_user_has_full_name()
    {
        $user = User::factory()->create([
            'first_name' => 'John',
            'last_name' => 'Doe',
        ]);

        $this->assertEquals('John Doe', $user->full_name);
    }

    public function test_user_can_be_activated()
    {
        $user = User::factory()->create(['active' => false]);
        
        $user->activate();
        
        $this->assertTrue($user->fresh()->active);
    }
}

Mocking Dependencies

// tests/Unit/PaymentServiceTest.php
namespace Tests\Unit;

use Tests\TestCase;
use App\Services\PaymentService;
use App\Services\PaymentGateway;
use Mockery;

class PaymentServiceTest extends TestCase
{
    public function test_processes_payment()
    {
        $gateway = Mockery::mock(PaymentGateway::class);
        $gateway->shouldReceive('charge')
            ->once()
            ->with(100, 'token123')
            ->andReturn(['success' => true]);

        $service = new PaymentService($gateway);
        $result = $service->processPayment(100, 'token123');

        $this->assertTrue($result['success']);
    }
}

Writing Feature Tests

Testing Routes and Controllers

// tests/Feature/PostControllerTest.php
namespace Tests\Feature;

use Tests\TestCase;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

class PostControllerTest extends TestCase
{
    use RefreshDatabase;

    public function test_can_list_posts()
    {
        $response = $this->get('/posts');

        $response->assertStatus(200);
        $response->assertViewIs('posts.index');
    }

    public function test_can_create_post()
    {
        $user = User::factory()->create();
        $this->actingAs($user);

        $response = $this->post('/posts', [
            'title' => 'Test Post',
            'content' => 'Test content',
        ]);

        $response->assertRedirect('/posts');
        $this->assertDatabaseHas('posts', [
            'title' => 'Test Post',
            'user_id' => $user->id,
        ]);
    }
}

Testing API Endpoints

// tests/Feature/ApiTest.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_can_get_user_data()
    {
        $user = User::factory()->create();
        
        $response = $this->actingAs($user, 'api')
            ->getJson('/api/user');

        $response->assertStatus(200)
            ->assertJson([
                'id' => $user->id,
                'email' => $user->email,
            ]);
    }

    public function test_requires_authentication()
    {
        $response = $this->getJson('/api/user');

        $response->assertStatus(401);
    }
}

Database Testing

Using Database Transactions

Laravel automatically wraps tests in database transactions:

// RefreshDatabase trait handles transactions
use Illuminate\Foundation\Testing\RefreshDatabase;

class ExampleTest extends TestCase
{
    use RefreshDatabase; // Database is reset after each test
}

Factories and Seeders

// Using factories
$user = User::factory()->create();
$users = User::factory()->count(10)->create();

// Using factories with relationships
$post = Post::factory()
    ->for(User::factory())
    ->create();

// Using seeders
$this->seed(); // Run all seeders
$this->seed(DatabaseSeeder::class); // Run specific seeder

Testing Best Practices

Test Organization

  • Keep tests organized by feature
  • Use descriptive test names
  • One assertion per test (when possible)
  • Test edge cases and error conditions

Naming Conventions

// Good test names
public function test_user_can_login_with_valid_credentials()
public function test_user_cannot_login_with_invalid_password()
public function test_guest_cannot_access_admin_panel()

// Bad test names
public function test1()
public function test_login()
public function test_user()

Test Coverage Goals

  • Aim for 80%+ coverage on critical paths
  • Focus on business logic first
  • Test user-facing features thoroughly
  • Don't obsess over 100% coverage

Common Testing Scenarios

Testing Forms and Validation

public function test_validation_errors_on_invalid_input()
{
    $response = $this->post('/register', [
        'email' => 'invalid-email',
        'password' => '123',
    ]);

    $response->assertSessionHasErrors(['email', 'password']);
}

public function test_form_submission_succeeds()
{
    $response = $this->post('/contact', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'message' => 'Test message',
    ]);

    $response->assertRedirect('/contact')
        ->assertSessionHas('success');
}

Testing Email Sending

use Illuminate\Support\Facades\Mail;

public function test_sends_welcome_email()
{
    Mail::fake();

    $user = User::factory()->create();

    // Trigger email sending
    event(new UserRegistered($user));

    Mail::assertSent(WelcomeEmail::class, function ($mail) use ($user) {
        return $mail->hasTo($user->email);
    });
}

Testing File Uploads

use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;

public function test_can_upload_file()
{
    Storage::fake('public');

    $file = UploadedFile::fake()->image('avatar.jpg');

    $response = $this->post('/profile/avatar', [
        'avatar' => $file,
    ]);

    Storage::disk('public')->assertExists('avatars/' . $file->hashName());
}

Testing Queues

use Illuminate\Support\Facades\Queue;

public function test_job_is_queued()
{
    Queue::fake();

    ProcessOrder::dispatch($order);

    Queue::assertPushed(ProcessOrder::class);
}

public function test_job_runs_synchronously()
{
    Queue::fake();

    ProcessOrder::dispatchSync($order);

    Queue::assertPushed(ProcessOrder::class);
    // Job runs immediately in tests
}

Continuous Integration

Running Tests in CI/CD

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
      
      - name: Install Dependencies
        run: composer install
      
      - name: Run Tests
        run: php artisan test

Conclusion

Laravel testing strategies are essential for building reliable, maintainable applications. By implementing comprehensive unit tests, feature tests, and integration tests, you can ensure your application works correctly and remains stable as it evolves.

Laravel's testing tools make it easy to write effective tests. With PHPUnit integration, powerful testing helpers, and excellent documentation, you have everything you need to implement a robust testing strategy.

Working with experienced Laravel developers who understand testing best practices can help you build applications that are reliable, maintainable, and ready for production.

Need Help with Testing?

Our team specializes in implementing comprehensive testing strategies for Laravel applications for businesses across Belfast, Dublin, and the Island of Ireland. We can help you set up test suites, improve code coverage, and ensure your application is reliable and maintainable. Looking for Laravel development services in Belfast or Dublin? Contact us today.

Related Articles

Laravel Deployment Strategies
Date Icon 10 July 2024
Guides

Laravel Deployment Strategies: From Development to Production

Learn how to deploy Laravel applications to production for businesses in Belfast, Dublin, and across the Island of Ireland. Complete guide covering server setup, deployment methods, environment configuration, and best practices for Laravel deployment.

Laravel API Development Best Practices
Date Icon 17 February 2023
Guides

Laravel API Development Best Practices: A Complete Guide for 2025

Master Laravel API development with this comprehensive guide for businesses in Belfast, Dublin, and across the Island of Ireland. Covering RESTful design, authentication, security, performance optimization, and testing strategies. Learn best practices from experienced Laravel developers.

Legacy Code Refactoring Laravel
Date Icon 7 April 2023
Guides

Legacy Code Refactoring: How to Modernize Your Laravel Application

Learn how to refactor legacy Laravel code to improve maintainability, performance, and security for businesses in Belfast, Dublin, and across the Island of Ireland. This guide covers refactoring strategies, common patterns, testing approaches, and when to hire a Laravel refactoring specialist.

STAY UPDATED WITH OUR LATEST ARTICLES

Don't miss out on our latest articles, product updates, and industry insights. Subscribe to get notified when we publish new content.