Skip to content
Go back

Why Laravel Uses a Separate Testing Database (And How It Works)

I ran my test suite and every single test failed. 184 failures, all with the same error:

SQLSTATE[HY000] [1049] Unknown database 'testing'

The fix took one command. But the real question — what is the testing database and why does Laravel need it? — turned out to be a much more interesting answer.

Table of contents

Open Table of contents

The one-line fix

The MySQL database called testing didn’t exist on my local machine. That’s it. Every test was trying to connect to a database that wasn’t there.

mysql -u root -h 127.0.0.1 -e "CREATE DATABASE IF NOT EXISTS testing;"

All 184 tests passed immediately after. But to understand why this is needed, you have to understand how Laravel’s test environment works.

What the testing database is for

When you develop your app, you work with a real database full of real data — users, teams, experiments. Your tests also need a database, because they create fake records, update them, delete them, and then check the results.

If tests used your real database, they’d mess up all your actual data every time they ran. So Laravel’s convention is: use a completely separate, empty database just for tests. Think of it like a scratch pad — tests scribble all over it, then wipe it clean for the next test. Your real database is never touched.

How it works — step by step

Step 1: phpunit.xml swaps your database

Every Laravel project has a phpunit.xml file that overrides your .env values when tests run. The key line is:

<env name="DB_DATABASE" value="testing"/>

This tells Laravel to connect to a database called testing instead of your real one (in my case, my_app). It also swaps out other services so tests are fast and don’t trigger real side effects:

<env name="APP_ENV" value="testing"/>
<env name="CACHE_STORE" value="array"/>
<env name="MAIL_MAILER" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>

Cache becomes in-memory (nothing persists). Mail becomes an array (nothing sends). Queues become synchronous (jobs run immediately). The Laravel testing docs explain this as a deliberate design choice — Laravel automatically configures these services so that no session or cache data will be persisted while testing.

Step 2: LazilyRefreshDatabase keeps each test isolated

My base TestCase.php uses the LazilyRefreshDatabase trait:

abstract class TestCase extends BaseTestCase
{
    use LazilyRefreshDatabase;
}

This trait does three things:

  1. Before the first test that needs the database, it runs all your migrations to build the schema (tables, columns, indexes) inside the testing database.
  2. Each test runs inside a database transaction. When the test finishes, the transaction is rolled back — so any records created during the test disappear instantly.
  3. The “lazily” part: if a test doesn’t touch the database at all (for example, it’s testing a pure function), the migrations never run. This saves time.

The result: every test starts with a clean, empty database. Test A can’t leave behind data that breaks Test B.

Step 3: Roles and permissions are seeded after migration

My TestCase.php also runs a setup step after migrations complete:

Event::listen(events: MigrationsEnded::class, listener: function () {
    Artisan::call('app:update-roles-and-permissions');
});

This populates the Spatie permission tables so authorisation checks work during tests. Without it, any test that checks “can this user do X?” would fail because the roles don’t exist.

RefreshDatabase vs LazilyRefreshDatabase

Laravel offers two versions of this trait. Here’s the difference:

TraitWhen migrations runBest for
RefreshDatabaseBefore every test class, regardlessTest classes that always use the database
LazilyRefreshDatabaseOnly when a test actually queries the databaseBase TestCase where some tests may not need the database

LazilyRefreshDatabase was introduced in PR #38861 as a drop-in replacement. The key benefit: you can safely add it to your base TestCase without slowing down tests that don’t touch the database.

Why not just use SQLite or an in-memory database?

Some tutorials suggest using SQLite for tests because it’s faster to set up. But there’s a meaningful tradeoff — SQLite handles SQL differently from MySQL. Column types, JSON queries, and certain constraints behave differently. Your tests can pass on SQLite but fail in production on MySQL.

By using the same database engine for testing as you do in production, you catch real-world SQL issues before they reach your users.

The questions I asked

When I first saw the error, I asked:

What is this ‘testing’ database used for?

The answer:

It’s the database your tests use when they run. Your phpunit.xml sets DB_DATABASE=testing, which overrides whatever database name is in your .env file. When tests run, Laravel connects to this separate testing database instead of your real database. This means tests can create, modify, and delete data freely without touching your actual app data.

I then asked for a deeper explanation:

Tell me more about this. Explain in detail but using simple terms for a non-developer.

The key insight from that answer:

Think of it like a scratch pad — tests scribble all over it, then wipe it clean for the next test. Your real database is never touched.

What the Laravel community says

This isn’t a niche pattern — it’s a core convention that every major Laravel figure follows.

Taylor Otwell (Laravel creator) designed this as a first-class convention. Every new Laravel project ships with DB_DATABASE=testing in phpunit.xml. When he built Laravel Sail, he made it automatically create a second database called testing alongside the main one. Taylor also personally merged the LazilyRefreshDatabase PR and multiple follow-up fixes, showing he actively maintains this testing infrastructure.

Nuno Maduro (creator of Pest PHP) designed Pest’s testing conventions around this pattern. In Pest, you typically apply uses(RefreshDatabase::class)->in('Feature') to all feature tests at once.

Freek Van der Herten (Spatie) follows this exact pattern in all Spatie packages and co-created the Testing Laravel video course.

Jason McCreary wrote Confident Laravel and the laravel-test-assertions package. His philosophy: tests should be fast enough that you actually want to run them.

Christoph Rumpel uses RefreshDatabase throughout his widely-referenced guide Everything You Can Test In Your Laravel Application, assuming a separate test database as a given.

What would Taylor Otwell do?

Taylor would use exactly this setup:

  1. Separate testing database configured in phpunit.xml
  2. LazilyRefreshDatabase on the base TestCase so every test gets a clean slate
  3. MySQL for tests, not SQLite so tests catch the same issues production would
  4. Environment overrides in phpunit.xml with cache, mail, queue, and session all set to array or sync

The one thing he’d nudge you on: in Laravel Sail, the testing database is created automatically. If you’re using Laravel Herd (like I am), you need to create it yourself — which is the gap that caused my 184 test failures in the first place.

Key takeaway

A separate testing database is Laravel’s way of letting you test fearlessly. Your tests can create, update, and delete any data they want without ever touching the database you use for development. The phpunit.xml file handles the swap automatically, LazilyRefreshDatabase keeps each test isolated, and the whole thing is designed to be invisible once it’s set up. The only catch — as I learned the hard way — is that the database has to actually exist first.


Back to top ↑