How to create an in-memory database for PHPUnit testing?

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company
# How to Create an In-Memory Database for PHPUnit Testing in Laravel As developers, one of the most common pain points when building robust test suites is managing database state. We need tests to be fast, isolated, and perfectly reproducible. When dealing with relational databases, setting up a full connection, running migrations, and seeding data for every single unit or feature test can introduce significant overhead. You are right to question relying solely on migrations for in-memory testing. While migrations define the structure (schema), they aren't ideal for ephemeral testing because they inherently rely on specific database drivers being available and often involve slower setup routines than necessary for pure data testing. This guide will walk you through the best, most efficient way to create isolated, in-memory-like database environments within a Laravel context for your PHPUnit tests, focusing on speed and isolation. ## The Problem with Traditional Setup The default Laravel testing setup often uses traits like `RefreshDatabase`, which executes all defined migrations. While this ensures your test environment mirrors the production structure, it forces actual database interaction, which is slow and ties your tests to a specific database driver (like MySQL or PostgreSQL). For pure unit testing or integration tests where you only care about Eloquent models and relationships, this is often overkill. ## The Best Practice: Seeding Data via Factories and Transactions The most efficient approach in Laravel is to leverage the power of **Database Factories** combined with **Database Transactions**. This method allows you to set up necessary data directly within the test scope without relying on complex schema setup or slow physical database initialization for every run. ### 1. Utilizing Database Factories Instead of running migrations, we focus on seeding *data*. Laravel’s Eloquent Model Factories are designed precisely for this purpose. You define factories that generate realistic, predictable data for your models. When writing a test, you can use the factory methods to create the exact state required for the test: ```php // Example in a Feature Test use App\Models\User; public function test_user_can_be_created_and_found() { // Create data directly using the factory $user = User::factory()->create([ 'name' => 'Test User', 'email' => 'test@example.com', ]); // Assertions follow... $this->assertNotNull($user); } ``` This approach keeps your tests focused on the application logic (what the code *does*) rather than the infrastructure setup (how the database is initialized). This philosophy aligns perfectly with the clean architecture promoted by solutions like those found at [laravelcompany.com](https://laravelcompany.com). ### 2. Ensuring Isolation with Database Transactions To ensure that a test doesn't leave behind unwanted data that pollutes subsequent tests, we use database transactions. When running tests, Laravel wraps each test in a transaction. If the test fails, the transaction is rolled back, instantly reverting the database to its original state. This achieves an "in-memory" effect for testing purposes—the data exists only for the duration of the test and is completely erased afterward. You can explicitly manage transactions within your tests or rely on Laravel's built-in hooks: ```php use Illuminate\Support\Facades\DB; public function test_data_is_rolled_back() { // Setup initial data (if necessary) $initialCount = DB::table('users')->count(); $this->assertEquals(0, $initialCount); // Perform an action that inserts data DB::table('users')->insert([ 'name' => 'Temporary', 'email' => 'temp@test.com' ]); // The transaction will automatically roll back upon test completion if using standard setup, // or you can explicitly roll it back for verification: DB::rollBack(); $this->assertEquals(0, DB::table('users')->count()); } ``` ## Conclusion: Speed and Isolation are Key For creating fast, reliable testing environments in Laravel, the key is to shift your focus from managing physical database structures (migrations) to managing test data state (factories and transactions). By using factories within transactional boundaries, you achieve the goal of an "in-memory" feel—data is created, used, and instantly erased—providing maximum speed and isolation for your PHPUnit suite. This approach ensures that your tests remain fast, self-contained, and highly reliable as you build complex applications on Laravel.