近期公司專案需要使用到 multi-tenancy 架構,所以我想分享一下使用這個架構時的測試經驗。
我使用了 Laravel Tenancy V3 套件來實現多租戶功能。在寫測試時,我們依照這個套件並新增測試。
Testing
新增 Trait 方便測試與使用
<?php
namespace Tests\Traits;
use App\Models\TenantModel;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;
use Stancl\Tenancy\Events\SyncedResourceSaved;
trait InteractsWithTenancy
{
public array $tenants = [];
public string $defaultTenant = 'domainA';
private static bool $migrated = false;
protected string $databasePrefix = 'test_tenant_';
protected string $domainPrefix = 'test_tenant_';
protected bool $disableSyncMaster = true;
public function setUpInteractsWithTenancy(): void
{
$databasePrefix = $this->databasePrefix;
config(['tenancy.database.prefix' => $databasePrefix]);
$this->tenants = empty($this->tenants) ? ['domainA', 'domainB'] : $this->tenants;
foreach ($this->tenants as $tenant) {
// 檢查有沒有資料庫,有的話就不建立
$manager = (new TenantModel)->database()->manager();
if ($manager->databaseExists($databasePrefix.$tenant)) {
$model = TenantModel::withoutEvents(fn () => TenantModel::firstOrCreate(['id' => $tenant]));
} else {
$model = TenantModel::firstOrCreate(['id' => $tenant]);
}
$model->domains()->firstOrCreate(['domain' => "{$this->domainPrefix}$tenant"]);
}
$this->tearDownInteractsWithTenancy();
if ($this->disableSyncMaster) {
$this->disableSyncMaster();
}
if ($this->defaultTenant) {
$this->switchTenant($this->defaultTenant);
} else {
$this->switchToCentral();
}
}
public function tearDownInteractsWithTenancy(): void
{
if ($this->hasRefreshDatabase()) {
if (! self::$migrated) {
$this->artisan('tenants:migrate-fresh');
$this->app[Kernel::class]->setArtisan(null);
self::$migrated = true;
}
}
}
// 取消 central 和 tenant 同步功能
public function disableSyncMaster()
{
Event::fake([
SyncedResourceSaved::class,
]);
}
public function switchTenant(string $tenant): void
{
$model = TenantModel::find($tenant->value);
if (! $model) {
throw new \Exception('Tenant not found');
}
tenancy()->initialize($model);
//確保 api 呼叫也可以正常切換
config(['app.url' => "http://{$this->domainPrefix}{$tenant->value}"]);
URL::forceRootUrl(config('app.url'));
}
public function switchToCentral(): void
{
tenancy()->end();
config(['app.url' => 'http://localhost']);
URL::forceRootUrl(config('app.url'));
}
private function hasRefreshDatabase()
{
$uses = array_flip(class_uses_recursive(static::class));
return isset($uses[RefreshDatabase::class]);
}
private function actingAsUser(\App\Models\UserModel|array|null $user = null, ?string $guard = null)
{
if (! $user || is_array($user)) {
$user = \App\Models\UserModel::factory()->create($user ?? []);
}
$this->actingAs($user, $guard);
return $user;
}
}
程式碼也放在這邊