Master integration testing in Laravel. Learn how to manage database state, verify multi-component workflows, and ensure your application logic works in unison.
Previously in this course, we explored Testing Events and Jobs in Laravel to ensure our asynchronous processes were firing correctly. Now, we're zooming out to the "Integration" level, where we verify that our services, repositories, and database constraints actually play nice together.
When you unit test, you mock everything. But in a real-world Laravel application, your code lives and dies by its side effects: a row created in a table, a relationship established between models, and a state change that ripples across your system. Integration testing is about ensuring these connections aren't just theoretically correct, but practically functional.
In our project board application, a "Task Creation" workflow involves the TaskService, the TaskRepository, and several database-level constraints (like project ownership). If we only mock the repository, we might miss a scenario where a database constraint violation—like a foreign key mismatch—breaks the user experience.
Integration testing allows us to:
task->project returns the expected instance.The biggest pain point in integration testing is database pollution. You don't want tests to leak state into each other. Laravel handles this natively using the Illuminate\Foundation\Testing\RefreshDatabase trait.
When you include this trait in your test class, Laravel performs two steps:
Let’s test our TaskService in a real integration scenario. We want to verify that when a task is created, it is correctly associated with the project and that our business logic (like setting a default status) is applied.
PHPnamespace Tests\Integration; use Tests\TestCase; use App\Models\Project; use App\Models\User; use App\Services\TaskService; use Illuminate\Foundation\Testing\RefreshDatabase; class TaskWorkflowTest extends TestCase { use RefreshDatabase; public function test_task_creation_workflow_persists_correctly() { #6A9955">// 1. Setup: Use factories to prepare the environment $user = User::factory()->create(); $project = Project::factory()->for($user)->create(); $service = app(TaskService::class); #6A9955">// 2. Action: Execute the service method $task = $service->createTask($project, [ 'title' => 'Complete documentation', 'description' => 'Write the API guide' ]); #6A9955">// 3. Assertion: Verify the database state directly $this->assertDatabaseHas('tasks', [ 'id' => $task->id, 'project_id' => $project->id, 'status' => 'pending' #6A9955">// Our default business logic ]); $this->assertEquals(1, $project->tasks()->count()); } }
By using assertDatabaseHas, we aren't just checking if the object exists in memory; we are querying the database to ensure the persistence layer captured the data exactly as intended.
Using the project board we’ve been building:
php artisan make:test Integration/ProjectAssignmentTest.RefreshDatabase trait.Project and two Users.ProjectService to assign the second user to the project.project_user pivot table contains the correct record.php artisan test.Even senior engineers run into these issues when writing integration tests:
Mockery or Event::fake() inside an integration test, ask yourself if you're still testing the integration. Keep integration tests focused on the real code interaction. Reference Mocking Services and Repositories in Laravel Tests to understand when to shift from mocking to true integration.setUp() method. Use Mastering Laravel Database Factories and Seeding for Testing to create minimal, specific data for the test at hand.DB::transaction, make sure your test doesn't accidentally commit data that breaks subsequent tests. Laravel’s RefreshDatabase is generally smart enough, but complex manual transactions can sometimes interfere with the test runner's rollback mechanism.Integration testing bridges the gap between isolated logic and the real-world behavior of your application. By leveraging the RefreshDatabase trait and focusing on database assertions, you ensure that your services, repositories, and database schema work together as a cohesive system. This level of testing is your best defense against regression in complex workflows.
Up next: We will dive into testing API authentication to ensure our protected endpoints remain secure.
Learn to master Laravel database factories and seeders to generate realistic test data. Create state modifiers and automate your testing workflow efficiently.
Read moreStop manually testing your forms. Learn how to use Laravel's testing suite to automate validation checks, simulate authenticated users, and ensure data integrity.
Advanced Testing: Integration Tests