Tangible Bytes

A Web Developer’s Blog

Laravel Database Testing

I wanted to better understand what is happening when I run Laravel tests that hit the database.

TLDR: Database migrations are run on every test run, optionally with seed data.

Each test case runs in a transaction.

(This is written based on Laravel 9)

Unit Vs Integration Tests

Unit test
Tests a single unit of code like a class or function. Doesn’t do things like hit the database - mocks could be used to enable this.
Unit tests run fast and don’t break except when the tested unit changes.
unit tests are isolated from each other - it doesn’t matter what order they run in - and if one fails it wont cause another to fail
Integration Test
Tests that things work together - allows things like database calls to go ahead.
Slower to run and more likely to break when something changes - but also more likely to catch problems.

Laravel creates folders for Unit and “Feature” (or integration) tests - there isn’t anything special about these folders. It is just a suggestion to keep the different types of test separate so that you can easily run just one type or the other.

There is a lot of talk about which type you should have more of - the main thing is having tests and being aware of the strengths and weaknesses of each type.

Database Testing in Laravel

Do read the documentation Laravel Database Testing

Laravel can run in a different environment during testing (specify this in phpunit.xml, or on the command line) so you can run using a specific test database and some of the code/documentation refers to this as “the test database” - as far as I can tell the test database is your regular database unless you setup something different.

But if you use the Illuminate\Foundation\Testing\RefreshDatabase trait in a test a few things happen

Migrations

At the start of the test this is called

    /**
     * Refresh a conventional test database.
     *
     * @return void
     */
    protected function refreshTestDatabase()
    {
        if (! RefreshDatabaseState::$migrated) {
            $this->artisan('migrate:fresh', $this->migrateFreshUsing());

            $this->app[Kernel::class]->setArtisan(null);

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }

The first test will trigger a database migration - updating your schema.

So any test run also updates the schema.

Test Isolation

From above :

        $this->beginDatabaseTransaction();

Each test runs in a transaction.

Everything works as normal but is rolled back at the end of the test.

Note though that auto-increment IDs don’t roll back and any data inserted after testing will get a higher auto increment ID that it would have without the tests.

Seed data

You can run seeders in each test

Or you can decide that you always want to run them in any test that uses RefreshDatabase and set

 protected $seed = true;

In your BaseTestCase - then seeding will run for every test class that uses the RefreshDatabase trait.

Summary

I’m documenting as I learn - but this is what I think happens.

I don’t have a separate test database.

I use RefreshDatabase on most test classes and have set protected $seed = true; in teh base class.

My database will be refreshed to the latest schema and seeded when tests run - these changes persist after tests complete.

Each test occurs in a transaction - isolated from other tests and with any changes rolled back at the end. Data changes made by tests do not persist.

Read the main docs Laravel Database Testing