在从 MySQL 迁移到 Postgres 的过程中,我们的 Laravel PHP 单元测试遇到了这个问题。
我们在
setUp()
或播种函数中使用播种机、工厂或模型将数据插入到我们的数据库中进行测试。
我们在所有测试中都在超类中使用
RefreshDatabase
特征。
然后在各个测试函数中,我们使用上述相同方法创建更多数据库记录。
当测试函数运行时,我们收到此错误(该消息根据我们正在测试的当前函数引用的实体略有不同)。
Illuminate\Database\QueryException
SQLSTATE[23505]: Unique violation: 7 ERROR: duplicate key value violates unique constraint "business_types_pkey"
DETAIL: Key (id)=(1001) already exists. (SQL: insert into "business_types" ("tenant_id", "name", "slug", "updated_at", "created_at") values (1001, Business Type with Empty Slug, , 2024-05-16 13:20:47, 2024-05-16 13:20:47) returning "id")
这是我们用于 BusinessTypes 实体的播种器
<?php
namespace Database\Seeders;
use App\Enums\BusinessTypeSlugEnum;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use App\Tenant;
class BusinessTypeSeeder extends Seeder
{
public const B2C_ID = 1001;
public const ECOMM_ID = 1002;
public const B2B_ID = 1003;
public const BUSINESS_TYPES = [
self::B2C_ID => [
'name' => 'B2C',
'slug' => BusinessTypeSlugEnum::B2C->value
],
self::ECOMM_ID => [
'name' => 'eComm',
'slug' => BusinessTypeSlugEnum::Ecomm->value
],
self::B2B_ID => [
'name' => 'B2B',
'slug' => BusinessTypeSlugEnum::B2B->value
]
];
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
foreach (self::BUSINESS_TYPES as $id => $columns) {
DB::table('business_types')->insert([
'id' => $id,
'name' => $columns['name'],
'slug' => $columns['slug'],
'tenant_id' => Tenant::firstOrFail()->id
]);
}
}
}
这是失败的测试功能
public function testResolveWhenBusinessTypeHasEmptySlugDiscoveryShouldNotHaveAnyServices(): void
{
$this->set_auth();
// Set the client's business type to one with an empty slug.
$business_type = BusinessType::create([
'tenant_id' => Tenant::firstOrFail()->id,
'name' => 'Business Type with Empty Slug',
'slug' => ''
]);
$client = Client::findOrFail(SingleClientSeeder::CLIENT_ID);
$client->business_type_id = $business_type->id;
$client->save();
$args = [
'client_name' => SingleClientSeeder::NAME,
'tier_id' => TiersSeeder::TIER_ID,
'create_discovery' => 'yes'
];
$audit = $this->resolve($args);
$discovery = $audit->discovery;
// Assert that a Discovery was created with no Departments/Services.
$this->assertExactlyOneNotSoftDeletedModelInTable($discovery);
$this::assertEmpty($discovery->departments);
$this::assertEmpty($discovery->services);
$this->assertDatabaseCount('discovery_department', 0);
$this->assertDatabaseCount('discovery_service', 0);
}
我相信当我们使用
::create()
函数创建新的 BusinessType 时会失败。
从播种器中删除特定的
id
属性似乎可以解决该问题。
然而,这导致我们重构每一个播种器并进行测试,以确保我们没有使用硬编码的 ID,这是一项非常艰巨的工作。我们有数百个测试类,它们依赖于我们在数据库中播种的硬编码 ID。
有没有更简单的方法来解决这个问题?
似乎与postgres如何处理序列有关?我不确定为什么在记录创建期间对 id 进行编码不会增加序列。
这是 Laravel 的问题吗?
因为当你硬编码 id 时,postgres 和 laravel 无法处理主键序列更新,所以有 2 种解决方案。
现在我们使用选项 2,使用辅助类
<?php
namespace App;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Model;
class PostgresHelper
{
/**
* Reset the primary key sequence for a table given a model.
*
* We extract the table name from the model.
*
* Because postgres handles primary key sequences differently than MySQL, we need to reset the sequence for a table
* when we insert a record with a specific ID. This is because the sequence will not automatically increment to the next id.
*
* If the table has no records, the sequence will be set to 1001.
* If the table has records, the sequence will be set to the max id so that the next id upon insertion will be the max id + 1.
*
* @param string Model $model - The model to get the table name from for the sequence reset.
* @return void
*/
public static function resetPrimaryKeySequenceForTable(Model $model)
{
$tableName = $model->getTable();
$sql = "select setval(pg_get_serial_sequence('nova.{$tableName}', 'id'), (select greatest(max(id),1001) from nova.{$tableName}));";
DB::statement($sql);
}
}
这是一个在我们的测试函数中使用它的示例,用于在插入没有硬编码 ID 的数据之前重置序列
public function testResolveWhenClientIsEcommerceDiscoveryShouldHaveAllCacAndLtvAppraisalServices(): void
{
$this->set_auth();
// Ensure the client's business type is e-comm.
$client = Client::findOrFail(SingleClientSeeder::CLIENT_ID);
$client->tier_id = TiersSeeder::TIER_ID;
$ecomm_business_type = BusinessTypeSeeder::ECOMM_ID;
$client->business_type_id = $ecomm_business_type;
$client->save();
PostgresHelper::resetPrimaryKeySequenceForTable(new Department());
// Create a Department and Service; these should be excluded from the Appraisal.
DepartmentFactory::createDepartmentWithServices('Test', 1);
// Create Cac and LTV Departments/Services.
[$cac_department, $ltv_department] = $this->createEcommAuditDepartments();
$cac_appraisal_services_ids = $cac_department->services()
->whereHas('modules', fn ($q) => $q->where('slug', 'appraisal'))
->pluck('id')
->toArray();
$ltv_appraisal_services_ids = $ltv_department->services()
->whereHas('modules', fn ($q) => $q->where('slug', 'appraisal'))
->pluck('id')
->toArray();
$args = [
'client_name' => SingleClientSeeder::NAME,
'tier_id' => TiersSeeder::TIER_ID,
'create_discovery' => 'yes'
];
$result = $this->resolve($args);
$audit = Audit::findOrFail($result->id);
$discovery = $audit->discovery;
$expected_department_ids = [$cac_department->id, $ltv_department->id];
$actual_department_ids = $discovery->departments->pluck('id')->toArray();
$expected_service_ids = array_merge($cac_appraisal_services_ids, $ltv_appraisal_services_ids);
$actual_service_ids = $discovery->services->pluck('id')->toArray();
// Assert that a Discovery was created with CAC and LTV Depts. and all their Services.
$this::assertEquals($expected_service_ids, $actual_service_ids);
$this::assertEquals($expected_department_ids, $actual_department_ids);
}