Trick to prevent unit tests from touching the database in Laravel

Stop letting your unit tests interact with DB unknowingly and slow tests down

Before we get into the discussion (read: controversy) on whether unit tests should touch the DB, let me tell you it is not required that your unit tests cannot touch the database. It depends on your testing strategy. Some people are okay with touching the database in unit tests.

We, at Freshbits, isolate unit tests completely. If something needs to connect with the database, that lands in the Feature tests directory even though we are not making any HTTP requests to test the features at hand.

I know it sounds weird, but that's our testing strategy and is working well for us at the moment.

Preface

By default in Laravel, the tests inside the Unit directory run before the Feature tests as per the phpunit.xml file configuration.

So when you run the tests for the very first time, the database has no tables and that is what we want. But the next time you run them, the database will have the tables because the feature tests have run the migrations.

The problem

What is the problem?

Some of our Unit tests were playing with the database, without our knowledge, because we missed mocking respective classes. Initially, there were no test failures. We were missing out on the test running speed though.

But, once we started running tests on GitHub Actions, unit tests were failing because CI starts with a fresh instance every time.

While we updated those unit tests accordingly, the main problem was that tests weren't failing on the local server but on the CI.

The solution

Wipe the database before running the first Unit test. The feature tests are using the LazilyRefreshDatabase trait anyway so they will have all the tables they need.

Lots of theory. Let's look at the code. I will show the sample code using PEST as that is what we are using but the fundamentals are the same so this works with PHPUnit too.

Step 1

We use a separate TestCase classes for Unit tests and Feature tests. We do that in Pest.php file this way:

uses(UnitTestCase::class)->in('Unit');
uses(FeatureTestCase::class)->in('Feature');

// ...

Step 2

Next, add the code to wipe the database before running our unit tests. Here is how our UnitTestCase.php file looks:

<?php

declare(strict_types=1);

namespace Tests;

use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
use Illuminate\Support\Facades\Artisan;

abstract class UnitTestCase extends BaseTestCase
{
    use CreatesApplication;

    private static bool $dbWiped = false;

    protected function setUp(): void
    {
        parent::setUp();

        if (static::$dbWiped) {
            return;
        }

        if ('testing' === app()->environment()) {
            Artisan::call('db:wipe');
        }

        static::$dbWiped = true;
    }
}

This is self-explanatory. We are running the Laravel-provided db:wipe Artisan command to remove all the tables before the first unit test runs.

Peace

That's it. We covered one of the ways to prevent Unit tests from touching the database. There may be other ways too. Feel free to comment below if you have another better way to achieve the same.

Repeating again; It is not required to keep the DB untouched in your Unit tests. It depends on your testing strategy.

If you're someone who has not started writing automated tests yet, here is our short article to kickstart the journey.

Happy testing!