StepToMe's avatar

Phpunit test not running with multiple database setup

We have a multi-tenancy set up with one main and other tenant databases and having some problems running non-sqlite tests. Databases are mysql.

Currently trying to run tests against the main database and one tenant database. The connections in my model constructors are set like so:

BaseMainModel

$this->connection = config('database.main');

BaseTenantModel

$this->connection = config('database.tenant');

I observe the following:

Test are running fine with an sqlite in memory databse with the following settings in phpunit.xml:

    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="QUEUE_DRIVER" value="sync"/>
    <env name="APP_DEBUG" value="true"/>
    <env name="DB_CONNECTION" value="testing"/>
    <env name="DB_CONNECTION_TENANT" value="testing"/>

In config.database.connections

        'testing' => [
            'driver' => 'sqlite',
            'database' => ':memory:',
            'prefix' => '',
        ],

However, when running without the sqlite in memory database (so commenting out my two phpunit lines for <env name="DB_CONNECTION" value="testing"/> and <env name="DB_CONNECTION_TENANT" value="testing"/>) and running on my normal database, the tests start hanging after the first test.

So for example, if I have 5 tests in a file SomeTests.php, the first one completes, and the process hangs on the second one.

If I add the protected $connectionsToTransact = ['main', 'tenant']; to my TestCase class, even the first test doesn't complete.

Another thing to note is that I am overriding the refreshTestDatabase() method of the RefreshDatabase trait by creating my own trait and using laravel's trait. My trait looks like so:

trait RefreshDatabase
{
    use BaseRefreshDatabase;

    /**
     * Refresh the in-memory database.
     *
     * @return void
     */
    protected function refreshInMemoryDatabase()
    {
        $this->artisan('migrate');

        $path = 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'tenants';

        Artisan::call('migrate', [
            '--database' => 'testing',
            '--path' => $path
        ]);

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

    /**
     * Refresh a conventional test database.
     *
     * @return void
     */
    protected function refreshTestDatabase()
    {
        if (!RefreshDatabaseState::$migrated) {

            $this->artisan('migrate:fresh');

            $path = 'database' . DIRECTORY_SEPARATOR . 'migrations' . DIRECTORY_SEPARATOR . 'tenants';

            $this->artisan('migrate:fresh', [
                '--path' => $path,
                '--database' => config('database.tenant')
            ]);

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

            RefreshDatabaseState::$migrated = true;
        }

        $this->beginDatabaseTransaction();
    }
}

I tried dumping in my test's setUp function:

class RelationsTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp()
    {
        parent::setUp();
        dump("here");  // this is logged
        $this->product = factory(Product::class)->create();
        dump("product created"); // this is not. However I dumped in the model constructors and it did enter the Product constructor
        $this->someClass= factory(SomeClass::class)->create();
        $this->user = factory(User::class)->create();
        $this->otherClass = factory(OtherClass::class)->create();

        $this->yetAnotherClass= factory(YetAnotherClass::class)->create([
            'owner_id' => $this->user->id,
            'product_id' => $this->product->id,
            'someClass_id' => $this->someClass->id,
        ]);
    }
...

Interestingly, if I run every test in the file separately, they run fine. So something happens in between the tests that I don't understand!

If I run two basic tests with just assertTrue(true) (not using factories or touching the database), they run fine.

Help is much appreciated! Thanks :)

0 likes
7 replies
StepToMe's avatar

Upgarding from php 7.1 to php 7.2 and adding this line

protected $connectionsToTransact = ['tenant'];

to my base test case seems to have fixed this partially.

However, if I add the main connection, tests are not running

protected $connectionsToTransact = ['main', 'tenant'];

scottzirkel's avatar

I have a similar setup, only in my RefreshDatabase I am explicitly connecting to my tenant's database.

This is my refreshInMemoryDatabase() method:

protected function refreshInMemoryDatabase()
    {
        $this->artisan('migrate');

        $tenants = TenantModel::all();

        $tenants->each(function ($tenant) {
            $tenant->connect();
            Artisan::call('migrate', [
                '--database' => 'testing',
                '--path'     => 'database/migrations/tenant'
            ]);
            $tenant->disconnect();
            $this->app[Kernel::class]->setArtisan(null);
        });
    }

I have a connect & disconnect method on my tenant model that sets the config for the tenant connection & resets it to the main respectively.

I do the same thing in the refreshTestDatabase() method.

StepToMe's avatar

@scottzirkel Yes, this also works for me with an sqlite database, but there is some kind of an issue when running tests against the a mysql database, specifically because I have a foreign key constraint from the tenant to the main database.

I had two options - either to not clear the 'main' mysql database after tests, or to remove my foreign keys to my main database (I chose the second option). And I've opened an issue about this on Laravel https://github.com/laravel/framework/issues/23413 (which I just saw I received a response on)

scottzirkel's avatar

Ah! Yeah you can't have foreign keys referencing across two dbs.

After posting my solution, my sqlite tests started to fail, so that's fun. You can safely ignore me it seems.

scottzirkel's avatar

I haven't been able to work on it in a while (paying clients really get in the way of personal projects), but I'm pretty certain when I do revisit, I'm just going to start over with a different approach.

Here's my connect function, it's in a helpers.php file I created. Not the best spot for it, I just kind of stuck it there while trying it out.

if (!function_exists('tenant_connect')) {
    function tenant_connect($hostname, $username, $password, $database)
    {
        DB::purge('tenant');

        Config::set('database.connections.tenant.host', $hostname);
        Config::set('database.connections.tenant.database', $database);
        Config::set('database.connections.tenant.username', $username);
        Config::set('database.connections.tenant.password', $password);

        DB::reconnect('tenant');

        Schema::connection('tenant')->getConnection()->reconnect();
    }
}

I call that from the Tenant model here:

public function connect()
    {
        if (!$this->connected()) {
            tenant_connect(
                $this->hostname,
                $this->username,
                $this->password,
                $this->database
            );
        }
    }

The disconnect is just another purge of tenant and a reconnect to the default connection.

Like I said, pretty crude, but it does get the job done, outside of testing migrations that is.

Please or to participate in this conversation.