createMock(Connection::class); $connection->method('fetchOne')->willReturn(false); $connection->method('executeStatement')->willReturnCallback( static function () use (&$steps): int { $steps[] = 'create'; return 1; }, ); $creator = new TenantDatabaseCreator($connection, new NullLogger()); // TenantMigrator is final — we wrap via the TenantProvisioner interface // to verify the creator is called. Migration subprocess cannot be tested unitarily. $provisioner = new class($creator, $steps) implements TenantProvisioner { /** @param string[] $steps */ public function __construct( private readonly TenantDatabaseCreator $creator, private array &$steps, ) { } public function provision(string $databaseName): void { $this->creator->create($databaseName); $this->steps[] = 'migrate'; } }; $provisioner->provision('classeo_tenant_test'); self::assertSame(['create', 'migrate'], $steps); } #[Test] public function itPropagatesCreationFailure(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchOne')->willThrowException(new RuntimeException('Connection refused')); $creator = new TenantDatabaseCreator($connection, new NullLogger()); $migrator = new TenantMigrator('/tmp', 'postgresql://u:p@h/db', new NullLogger()); $provisioner = new DatabaseTenantProvisioner($creator, $migrator); $this->expectException(RuntimeException::class); $provisioner->provision('classeo_tenant_test'); } }