Faster Tests on “Tenancy for Laravel”

on Mutli-Database setups using Named SQLite Connections.

Faster Tests on “Tenancy for Laravel”
Photo by Dominik Scythe / Unsplash

If you're like me, fast feedback loops in development are crucial for mentally unblocking creative flow, keeping development cadence and ensuring quality code is maintained.

Having to wait over 10-30 seconds for tests may not sound like much, however when testing over 300-400 times per development session, every performance improvement matters – especially when trying to remove bugs from your application.

In the Laravel ecosystem, Laravel has integrated an in-memory SQLite Driver to speed up testing when interacting with databases. All you should have to do is set your database connection to SQLite, and your database to :memory:.

Unfortunately, when using Tenancy for Laravel, if you want to use multiple databases to separate out your tenants, this approach isn't supported, as the SQLite in-memory driver only supports a single database, quoting the reasoning below

Note: If you're using multi-database tenancy & the automatic mode, it's not possible to use :memory: SQLite databases or the RefreshDatabase trait due to the switching of default database.

The Guide to Faster Testing

To get around this limitation, we're going to leverage a tool called crhg/laravel-sqlite-named-memory-connection, which supports multiple in-memory databases using the SQLite Driver.

GitHub - crhg/laravel-sqlite-named-memory-connection: Introduce named SQLite in-memory database connection to Laravel
Introduce named SQLite in-memory database connection to Laravel - crhg/laravel-sqlite-named-memory-connection
⚠️
As of writing, there's no support for Laravel versions beyond version 7 on the official repo, so I forked it and added support up to Laravel 12.
https://github.com/crhg/laravel-sqlite-named-memory-connection/pull/5

Let's get started. Firstly, install the required package that will allow for a new database connection that does in fact support our multiple database requirement.

# Install the custom SQLite Named Memeory Connections repository for Laravel 9+
composer config repositories.laravel-sqlite-named-memory-connection vcs https://github.com/sifex/laravel-sqlite-named-memory-connection.git

# Install the package
composer require crhg/laravel-sqlite-named-memory-connection@dev-master --dev

Once installed, add your sqlite-named driver to the databases.php file, with the following configuration:


    'connections' => [
        ...
        
        'sqlite-named' => [
            'driver' => 'sqlite-named',
            'url' => env('DATABASE_URL'),
            'database' => env('DB_DATABASE', database_path('database.sqlite')),
            'prefix' => '',
            'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
        ],
        

Create a new file called SQLiteInMemoryDatabaseManager.php somewhere in your application. In this example, I used a folder called "App\Tenancy".

<?php

namespace App\Tenancy;

use Stancl\Tenancy\Contracts\TenantWithDatabase;
use Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager;

class SQLiteInMemoryDatabaseManager extends SQLiteDatabaseManager
{
    public function createDatabase(TenantWithDatabase $tenant): bool
    {
        return true;
    }

    public function deleteDatabase(TenantWithDatabase $tenant): bool
    {

        return true;
    }

    public function databaseExists(string $name): bool
    {
        return false;
    }

    public function makeConnectionConfig(array $baseConfig, string $databaseName): array
    {
        $baseConfig['database'] = ':named-memory:'.database_path($databaseName);

        return $baseConfig;
    }

    public function setConnection(string $connection): void
    {
        //
    }
}

Update your Database Manager in tenancy.php to now support sqlite-named :

...

    'database' => [ 
        'managers' => [
            'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
            'sqlite-named' => SQLiteInMemoryDatabaseManager::class, # Adding this
            'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
            'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
            
...

Edit your phpunit.xml file to include the new connection, making sure replace "central" with the expected name of your testing database.

    <php>
        ...
        <env name="DB_CONNECTION" value="sqlite-named"/>
        <env name="DB_DATABASE" value=":named-memory:central"/>
        ...
    </php>

Lastly, we need to supply a custom version of the RefreshDatabase trait, that supports the deletion of the database. I've supplied a version called RefreshDatabaseWithTenant here, but you may need to modify this for your own use.

<?php

namespace Tests\Traits;

use App\Models\Tenant;
use Illuminate\Foundation\Testing\RefreshDatabase;

trait RefreshDatabaseWithTenant
{
    use RefreshDatabase {
        beginDatabaseTransaction as parentBeginDatabaseTransaction;
    }

    /**
     * The database connections that should have transactions.
     *
     * `null` is the default landlord connection
     * `tenant` is the tenant connection
     */
    protected array $connectionsToTransact = [null, 'sqlite-named'];

    /**
     * We need to hook initialize tenancy _before_ we start the database
     * transaction, otherwise it cannot find the tenant connection.
     */
    public function beginDatabaseTransaction()
    {
        $this->initializeTenant();

        $this->parentBeginDatabaseTransaction();
    }

    public function initializeTenant()
    {
        $baseDomain = 'domain.test';
        $tenantId = \Illuminate\Support\Str::random(10);
        $tenantDomain = $tenantId.'.'.$baseDomain;

        // Delete all tenants.
        Tenant::all()->each->delete();

        // Retrieve or create the tenant with the given ID.
        $tenant = Tenant::firstOr(function () use ($tenantId, $tenantDomain) {

            // Create the tenant and associated domain if they don't exist.
            $t = Tenant::factory()->create(['id' => $tenantId]);

            if (! $t->domains()->count()) {
                $t->domains()->create(['domain' => $tenantDomain]);
            }

            return $t;
        });

        // Initialize tenancy for the current test.
        tenancy()->initialize($tenant);
    }
}

RefreshDatabaseWithTenant.php

Then in each test that interacts with your multi-tenancy, you can add the trait to your test file. Below is an example for one used in Pest

uses(\Tests\Traits\RefreshDatabaseWithTenant::class);

🎉 That should be it. You should now have faster tests in Tenancy for Laravel using In-Memory SQLite databases.

How much faster is it?

On Pest v3, we see that we're about 20% faster on non-parallelized tests, and around 41% faster on parallelized tests. When dealing with a larger size of codebase, this is a welcome change.

Regular SQLite (Non-Parallelized)
Named In-Memory SQLite (Non-Parallelized)
Regular SQLite (Parallelized)
Named In-Memory SQLite (Parallelized)

A better approach on the horizon

There does seem to be some awesome work on getting this operational in the future in version 4 of Tenancy for Laravel – as well as some work being published back to the Laravel core framework to make multiple in-memory SQLite databases supported.

I would love for this workaround to be officially supported, and definitely worth sponsoring to stanctl on Github if you haven't already.