使用 Postgres 进行 Laravel PHP 单元测试 - SQLSTATE[23505]:唯一违规:7 错误:重复的键值违反唯一约束已存在

问题描述 投票:0回答:1

在从 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 的问题吗?

php laravel postgresql phpunit
1个回答
0
投票

因为当你硬编码 id 时,postgres 和 laravel 无法处理主键序列更新,所以有 2 种解决方案。

  1. 重构所有播种函数/插入以不使用硬编码 ID,并修复引用它们的测试
  2. 每次插入带有 id 的硬编码数据,然后在事后插入不带 id 的数据时,都会调用一个函数来重置测试中的主键序列

现在我们使用选项 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);
}
© www.soinside.com 2019 - 2024. All rights reserved.