Faster MultiTenant Testing in Laravel

Josh Krawczyk
Jan 22 · 4 min read

This is about the day I spent optimizing my tests instead of my app. I just couldn’t take it anymore. My API tests would take over 10min to run. My first attempt at optimizing this process was separating the tests into groups, but this got out of hand quickly. So I finally sat down and figured out why the tests were slow and learned a lot along the way!

Check this article about my first attempt at getting this to work here.

Background

A little background on this app, I am utilizing the hyn\multi-tenant package for Laravel. Currently, a tenant is created before each test and the following happens:

On my local dev setup, this process will take about 30 secs per test.

I thought about how I can make this more efficient. The first thing I thought of was creating and maintaining a single tenant throughout all of the tests. This way I just run tenancy:migrate:fresh between tests to get fresh data. I found this didn’t help much because dropping all of the tenant tables and recreating them was most of the time consumed by these tests.

My next thought was to try database transactions. This way the database will be rolled back between tests. I tested this out and BINGO! Now I was looking at around 5sec per test. This is much easier to work with. Let’s get to some code.

The Code

I am utilizing a trait that handles all of the tenant testing operations. The first task was to build logic into the trait that would create a tenant if one didn’t already exist. See the following method:

protected function checkTenancy()
{
if ($this->hostnames->findByhostname($this->tenantUrl)) {
$this->setUpHostnames();
$this->setUpWebsites();
} else { $this->artisan('migrate:fresh', [
'--no-interaction' => 1,
'--force' => 1
]);
$this->setUpHostnames(true);
$this->setUpWebsites(true, true);
}
}

So it simply checks if the tenant url already exists and then loads the hostname and website. If the tenant doesn’t exist it runs fresh migrations on the system database. It then creates a new hostname and website and saves them to the system database. This is what the boolean flags (true, true) are doing.

Now we need to setup the database transaction for the system database. The hyn\multi-tenant package has some built in for this so I utilized that.

protected function setUpTenancy()
{
$this->websites = app(WebsiteRepository::class);
$this->hostnames = app(HostnameRepository::class);
$this->connection = app(Connection::class);
$this->checkTenancy(); $this->connection->system()->beginTransaction();}

Here we are grabbing instances of the Website and Hostname repositories and the Connection class. We are running the checkTenancy() method we created earlier. Then we are beginning the system database transaction. Easy! We will be using this method to initialize the tenancy system from our tests.

Something else to be aware of is we can use transactions with the Tenant database. Using a methods like this:

protected function activateTenant()
{
app(Environment::class)->tenant($this->website);
$this->connection->get()->beginTransaction();
}
protected function rollbackTenant()
{
if ($this->connection->exists() && $this->connection->get()->transactionLevel() > 0) {
$this->connection->get()->rollBack();
}
}

We will start a transaction after we activate the tenant. Then we can rollback the connection by calling rollbackTenant(). I am calling this in the cleanupTenancy() method.

Here is the entire trait.

Another method to note is the cleanUpTenancy(). Here we are rolling back the transaction.

Usage Example

Here is an example of how I used it in my tests. I utilized the SetUp() and tearDown() testing hooks to initialize and cleanup tenancy. Here is a TenantTestCase that your tenant tests can inherit from.

We are just calling the setUpTenancy() method in the setUp hook and activating the tenant so that all requests will use the tenant connect. I also have a duringSetUp() method that the tests can use.

Here is an actual test.

As you can see it looks like a normal test, but behind the scenes it is utilizing the multi-tenant package.

Now we can run./vendor/bin/phpunit — testdox and see the green check marks. Much better on the time too!

Conclusion

And that’s it! The goal was to get the tests to run faster and I think this is as good as I can do code-wise. I could get a better computer, but I’m not too worried about it. You can see the entire code demo below.

I hope you found this demo useful! If you liked this article check out my series on creating a multi-tenant application.

Thanks!

Josh Krawczyk

Written by

Systems Engineer by day, Programmer by night. Just doing my best to juggle life, family, career, and passion.