Advanced Laravel Testing Strategies: From Unit Tests to E2E Automation in 2025

Salman Hassan
May 27, 2025
6 min read
2 comments

Advanced Laravel Testing Strategies: From Unit Tests to E2E Automation in 2025

Testing has evolved dramatically in the Laravel ecosystem. With tools like Pest PHP, parallel testing capabilities, and advanced CI/CD integrations, we can now build more robust applications with greater confidence than ever before.

The Modern Laravel Testing Stack

1. Pest PHP: The Game Changer

Pest PHP has revolutionized how we write tests in Laravel. Its expressive syntax makes tests more readable and maintainable:

<?php

use App\Models\User;
use App\Models\Blog;

it('can create a blog post', function () {
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)->post('/blogs', [
        'title' => 'Test Blog',
        'content' => 'This is a test blog post',
    ]);
    
    $response->assertStatus(201);
    expect(Blog::count())->toBe(1);
});

it('validates required fields', function () {
    $user = User::factory()->create();
    
    $response = $this->actingAs($user)->post('/blogs', []);
    
    $response->assertSessionHasErrors(['title', 'content']);
});

2. Advanced Factory Patterns

Modern factories should handle complex relationships and states:

<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class BlogFactory extends Factory
{
    public function definition()
    {
        return [
            'title' => $this->faker->sentence(),
            'slug' => $this->faker->slug(),
            'content' => $this->faker->paragraphs(5, true),
            'author_id' => User::factory(),
            'status' => 'published',
        ];
    }

    public function draft()
    {
        return $this->state(['status' => 'draft']);
    }

    public function withComments($count = 3)
    {
        return $this->afterCreating(function ($blog) use ($count) {
            Comment::factory($count)->create(['blog_id' => $blog->id]);
        });
    }

    public function popular()
    {
        return $this->afterCreating(function ($blog) {
            $blog->increment('views', $this->faker->numberBetween(1000, 10000));
        });
    }
}

Parallel Testing for Speed

Laravel 10+ supports parallel testing out of the box:

# Run tests in parallel
php artisan test --parallel

# Specify number of processes
php artisan test --parallel --processes=4

Configure parallel testing in phpunit.xml:

<extensions>
    <extension class="ParaTest\Laravel\ParaTestBootstrap"/>
</extensions>

API Testing Best Practices

1. Comprehensive API Test Suite

<?php

describe('Blog API', function () {
    beforeEach(function () {
        $this->user = User::factory()->create();
        $this->blog = Blog::factory()->create(['author_id' => $this->user->id]);
    });

    describe('GET /api/blogs', function () {
        it('returns paginated blogs', function () {
            Blog::factory(15)->create();
            
            $response = $this->getJson('/api/blogs');
            
            $response->assertOk()
                ->assertJsonStructure([
                    'data' => [
                        '*' => ['id', 'title', 'excerpt', 'author', 'created_at']
                    ],
                    'links',
                    'meta'
                ]);
        });

        it('filters blogs by status', function () {
            Blog::factory()->draft()->count(3)->create();
            Blog::factory()->published()->count(5)->create();
            
            $response = $this->getJson('/api/blogs?status=published');
            
            expect($response->json('data'))->toHaveCount(6); // 5 + 1 from beforeEach
        });
    });

    describe('POST /api/blogs', function () {
        it('creates blog with proper validation', function () {
            $blogData = [
                'title' => 'New Blog Post',
                'content' => 'This is the content of the blog post.',
                'tags' => ['laravel', 'testing']
            ];
            
            $response = $this->actingAs($this->user)
                ->postJson('/api/blogs', $blogData);
            
            $response->assertCreated()
                ->assertJsonFragment(['title' => 'New Blog Post']);
            
            $this->assertDatabaseHas('blogs', [
                'title' => 'New Blog Post',
                'author_id' => $this->user->id
            ]);
        });
    });
});

2. Testing File Uploads

it('can upload blog featured image', function () {
    Storage::fake('public');
    
    $file = UploadedFile::fake()->image('featured.jpg', 800, 600);
    
    $response = $this->actingAs($this->user)
        ->postJson('/api/blogs', [
            'title' => 'Blog with Image',
            'content' => 'Content here',
            'featured_image' => $file
        ]);
    
    $response->assertCreated();
    
    $blog = Blog::latest()->first();
    expect($blog->featured_image)->not->toBeNull();
    Storage::disk('public')->assertExists($blog->featured_image);
});

Database Testing Strategies

1. Using Transactions for Speed

<?php

use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

// Or for specific tests
it('performs database operations', function () {
    $this->beginDatabaseTransaction();
    
    // Your test logic here
    
    $this->rollbackDatabaseTransaction();
});

2. Testing Database Relationships

it('loads blog with relationships efficiently', function () {
    $blog = Blog::factory()
        ->withComments(3)
        ->create();
    
    DB::enableQueryLog();
    
    $loadedBlog = Blog::with(['author', 'comments.user'])->find($blog->id);
    
    $queries = DB::getQueryLog();
    
    expect(count($queries))->toBeLessThanOrEqual(3); // Avoid N+1 queries
    expect($loadedBlog->comments)->toHaveCount(3);
});

Browser Testing with Laravel Dusk

<?php

use Laravel\Dusk\Browser;

it('can create and publish a blog post', function () {
    $user = User::factory()->create();
    
    $this->browse(function (Browser $browser) use ($user) {
        $browser->loginAs($user)
            ->visit('/admin/blogs/create')
            ->type('title', 'My New Blog Post')
            ->type('content', 'This is the content of my blog post.')
            ->select('status', 'published')
            ->click('@save-blog')
            ->assertPathIs('/admin/blogs')
            ->assertSee('Blog post created successfully');
    });
    
    $this->assertDatabaseHas('blogs', [
        'title' => 'My New Blog Post',
        'status' => 'published'
    ]);
});

CI/CD Integration

GitHub Actions Configuration

name: Laravel Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: testing
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

    steps:
    - uses: actions/checkout@v3
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: 8.2
        extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv

    - name: Install dependencies
      run: composer install --no-progress --prefer-dist --optimize-autoloader

    - name: Generate key
      run: php artisan key:generate

    - name: Run migrations
      run: php artisan migrate --env=testing

    - name: Run tests
      run: php artisan test --parallel

Performance Testing

it('handles concurrent blog creation', function () {
    $users = User::factory(10)->create();
    
    $startTime = microtime(true);
    
    collect($users)->each(function ($user) {
        $this->actingAs($user)->postJson('/api/blogs', [
            'title' => 'Concurrent Blog',
            'content' => 'Content for concurrent blog'
        ]);
    });
    
    $endTime = microtime(true);
    $executionTime = $endTime - $startTime;
    
    expect($executionTime)->toBeLessThan(5.0); // Should complete in under 5 seconds
    expect(Blog::count())->toBe(10);
});

Testing Best Practices for 2025

1. Test Organization

  • Group related tests using describe() blocks
  • Use meaningful test descriptions
  • Follow the AAA pattern (Arrange, Act, Assert)

2. Mock External Services

it('handles email service failure gracefully', function () {
    Mail::fake();
    Mail::shouldReceive('send')->andThrow(new Exception('Service unavailable'));
    
    $response = $this->postJson('/api/blogs', $validBlogData);
    
    $response->assertCreated();
    // Blog should be created even if email notification fails
});

3. Test Edge Cases

  • Empty data sets
  • Boundary conditions
  • Error scenarios
  • Race conditions

Conclusion

Modern Laravel testing in 2025 is about creating comprehensive, maintainable test suites that give you confidence in your code. By leveraging Pest PHP's expressive syntax, parallel testing for speed, and robust CI/CD integration, you can build applications that are both reliable and performant.

Remember: good tests are not just about coverage—they're about creating a safety net that allows you to refactor and iterate with confidence.

Start implementing these advanced testing strategies today and transform your development workflow.

Comments (2)

Leave a Comment
M
Mike Johnson
6 months ago

Pest PHP has really changed how I write tests. The parallel testing feature alone saves me so much time during CI/CD runs.

Reply to Mike Johnson
E
Emma Thompson
6 months ago

The factory patterns section is gold! I've been struggling with complex test data setup, and these examples solve exactly that problem.

Reply to Emma Thompson