Skip to content

Instantly share code, notes, and snippets.

@adarmanto
Last active May 31, 2023 00:53
Show Gist options
  • Save adarmanto/44e4b94eb570da05a619fd44c8dd9c05 to your computer and use it in GitHub Desktop.
Save adarmanto/44e4b94eb570da05a619fd44c8dd9c05 to your computer and use it in GitHub Desktop.
How to configure the unit tests for spatie/laravel-multitenancy package

The unit tests for spatie/laravel-multitenancy

Requirements

  • php >= 8.0
  • Laravel v9
  • spatie/laravel-multitenancy v2 using multiple databases
  • MySQL

Note

  • The database user must be able to have a permission to create database
  • This example is included with creating tenant database through queue
  • The implementation is using the booting trait helper
  • It is not supported for parallel testing (issue)
<?php
namespace App\Jobs;
use App\Models\Tenant;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Spatie\Multitenancy\Jobs\NotTenantAware;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
class CreateTenantDatabase implements ShouldQueue, NotTenantAware
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UsesMultitenancyConfig;
/**
* Create a new job instance.
*
* @param Tenant $tenant
* @return void
*/
public function __construct(public Tenant $tenant)
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$this->tenant->createDatabase();
}
}
<?php
namespace App\Models;
use App\Jobs\CreateTenantDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Artisan;
use Spatie\Multitenancy\Models\Tenant as BaseTenant;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
class Tenant extends BaseTenant
{
use UsesMultitenancyConfig;
protected static function booted()
{
// Create a tenant database through queue
static::created(fn(Tenant $tenant) => dispatch(new CreateTenantDatabase($tenant)));
}
/**
* Create and run tenant database migration through tenant db connection instead
*
* @return void
*/
public function createDatabase(): void
{
// Create database
DB::connection($this->tenantDatabaseConnectionName())->statement("CREATE DATABASE {$this->database}");
// Run db migration
Artisan::call("tenants:artisan migrate --tenant={$this->id}");
}
/**
* Drop tenant database
*
* @return void
*/
public function dropDatabase(): void
{
DB::connection($this->tenantDatabaseConnectionName())->statement("DROP DATABASE {$this->database}");
}
}
<?php
return [
'default' => env('DB_CONNECTION', 'tenant'),
'connections' => [
'tenant' => [
...
'database' => null,
...
],
'landlord' => [
...
'database' => env('DB_DATABASE_LANDLORD', ''),
...
],
]
];
<php>
...
<server name="DB_DATABASE_LANDLORD" value="testing_landlord_db"/>
<server name="DB_DATABASE_TENANT" value="testing_tenant_db"/>
<server name="QUEUE_CONNECTION" value="sync"/>
...
</php>
<?php
namespace Tests\Concerns;
use App\Jobs\CreateTenantDatabase;
use App\Models\Tenant;
use Illuminate\Foundation\Console\Kernel;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Queue;
use Spatie\Multitenancy\Concerns\UsesMultitenancyConfig;
use Spatie\Multitenancy\Events\MadeTenantCurrentEvent;
use Tests\MultitenancyState;
trait TenantAware
{
use UsesMultitenancyConfig;
/**
* Set up the multitenancy migration of landlord db
*
* @return void
*/
public function setUpTenantAware(): void
{
$this->refreshLandLordDatabase();
if ($this->setUpCurrentTenant()) {
$this->refreshTenantDatabase();
}
}
/**
* Refresh landlord database
*
* @return void
*/
protected function refreshLandLordDatabase(): void
{
$connectionName = $this->landlordDatabaseConnectionName();
if (! MultitenancyState::$landLordMigrated) {
$this->artisan("migrate:fresh --database={$connectionName} --path=database/migrations/landlord")
->assertExitCode(0);
$this->app[Kernel::class]->setArtisan(null);
MultitenancyState::$landLordMigrated = true;
}
$this->beginDatabaseTransaction($connectionName);
}
/**
* Refresh tenant database
*
* @return void
*/
protected function refreshTenantDatabase(): void
{
$connectionName = $this->tenantDatabaseConnectionName();
if (! MultitenancyState::$tenantMigrated) {
$this->artisan("migrate:fresh --database={$connectionName}")
->assertExitCode(0);
$this->app[Kernel::class]->setArtisan(null);
MultitenancyState::$tenantMigrated = true;
}
if (Tenant::checkCurrent()) {
// Start the transaction when the current tenant exists
$this->beginDatabaseTransaction($connectionName);
} else {
// Start the transaction until a tenant is being made the current one
Event::listen(MadeTenantCurrentEvent::class, fn () => $this->beginDatabaseTransaction($connectionName));
}
}
/**
* Begin a database transaction on the testing database.
*
* @return void
*/
public function beginDatabaseTransaction($connectionName)
{
$database = $this->app->make('db');
$connection = $database->connection($connectionName);
$dispatcher = $connection->getEventDispatcher();
$connection->unsetEventDispatcher();
$connection->beginTransaction();
$connection->setEventDispatcher($dispatcher);
if ($this->app->resolved('db.transactions')) {
$this->app->make('db.transactions')->callbacksShouldIgnore(
$this->app->make('db.transactions')->getTransactions()->first()
);
}
$this->beforeApplicationDestroyed(function () use ($database, $connectionName) {
$connection = $database->connection($connectionName);
$dispatcher = $connection->getEventDispatcher();
$connection->unsetEventDispatcher();
$connection->rollBack();
$connection->setEventDispatcher($dispatcher);
$connection->disconnect();
});
}
/**
* Set up the current tenant for application testing
*
* @return bool
*/
public function setUpCurrentTenant(): bool
{
Queue::fake([CreateTenantDatabase::class]);
$tenant = Tenant::factory(['database' => env('DB_DATABASE_TENANT')])->create();
$tenant->makeCurrent();
Queue::assertPushed(fn (CreateTenantDatabase $job) => $job->tenant->id == $tenant->id, 1);
return $tenant->isCurrent();
}
}
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Tenant;
use Tests\Concerns\TenantAware;
class ExampleTest extends TestCase
{
use TenantAware;
public function testExample()
{
$this->assertTrue(Tenant::checkCurrent());
}
}
<?php
namespace Tests\Feature\Jobs;
use Tests\TestCase;
use App\Models\Tenant;
use Tests\Concerns\TenantAware;
class ExampleWithoutCurrentTenantTest extends TestCase
{
use TenantAware;
/**
* Skip the current tenant
*/
public function setUpCurrentTenant(): bool
{
return false;
}
public function testExample()
{
$tenant = Tenant::factory(['database' => 'test_random_tenant_db'])->create();
$tenant->makeCurrent();
$this->assertTrue($tenant->isCurrent());
// Clean up the testing tenant db
$tenant->dropDatabase();
}
}
<?php
namespace Tests;
class MultitenancyState
{
/**
* Indicates if the landlord database has been migrated.
*
* @var bool
*/
public static $landLordMigrated = false;
/**
* Indicates if the tenant database has been migrated.
*
* @var bool
*/
public static $tenantMigrated = false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment