You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel+PostgreSQL环境下PestPHP测试用例报错的解决咨询

问题描述

本人习惯使用Laravel+MySQL技术栈,当前在采用Laravel+PostgreSQL的项目中运行PestPHP测试用例时遇到异常。

测试用例代码如下:

it('should not edit a game title if another game has the same title', function () {
    $contentManagerService = app(ContentManagerService::class);
    $existingGame = Game::factory()->create();
    $targetGame = Game::factory()->create();

    $editGameTitleResults = $contentManagerService
    ->setid($targetGame->id)
    ->setTitle($existingGame->title)
    ->editGameTitle();

    expect($editGameTitleResults)->toBeString();

    $actualGameRecord = Game::where('id', $targetGame->id)->get()->first();
    expect($actualGameRecord->title)->toBe($targetGame->title);
});

该测试用例要验证的逻辑是:尝试把某游戏标题改成另一已存在游戏的标题时,应该返回错误字符串,且目标游戏的标题不会被修改。这个用例在Laravel+MySQL环境下能正常运行,但在PostgreSQL环境里抛出了异常:

SQLSTATE[25P02]: In failed sql transaction: 7 ERROR: current transaction is aborted, commands ignored until end of transaction block

我已经使用了Illuminate\Foundation\Testing\RefreshDatabase trait,想知道是不是操作有问题,或者该怎么改写测试用例适配PostgreSQL环境。

问题原因与解决办法

核心差异

PostgreSQL和MySQL对事务失败后的处理逻辑存在本质区别:

  • MySQL执行失败的SQL语句后,会自动回滚该语句的操作,事务仍处于可用状态,后续查询可以正常执行。
  • PostgreSQL只要有一条SQL执行失败,整个事务就会被标记为中止状态,必须手动回滚事务,否则后续所有SQL命令都会报错。

你的测试中,editGameTitle()方法内部触发了违反唯一约束的SQL(标题重复),导致PostgreSQL事务中止,后续执行Game::where(...)查询时,事务仍处于无效状态,因此抛出异常。

两种解决方式

1. 在业务方法中捕获异常并回滚事务

修改ContentManagerService的editGameTitle()方法,遇到唯一约束异常时手动回滚事务,确保后续操作能正常进行:

public function editGameTitle()
{
    try {
        DB::beginTransaction();
        
        $game = Game::findOrFail($this->id);
        $game->title = $this->title;
        $game->save();
        
        DB::commit();
        // 返回成功状态或结果
    } catch (\Illuminate\Database\QueryException $e) {
        // PostgreSQL的唯一约束错误码为23505,匹配该异常
        if ($e->getCode() === '23505') {
            DB::rollBack();
            return '标题已存在'; // 与测试预期的返回字符串一致
        }
        // 其他异常正常抛出
        throw $e;
    }
}

2. 在测试中隔离事务或手动回滚

如果不想修改业务代码,可以将触发异常的操作放在独立的事务闭包里,或者在异常发生后手动回滚:

it('should not edit a game title if another game has the same title', function () {
    $contentManagerService = app(ContentManagerService::class);
    $existingGame = Game::factory()->create();
    $targetGame = Game::factory()->create();

    $editGameTitleResults = DB::transaction(function () use ($contentManagerService, $targetGame, $existingGame) {
        try {
            return $contentManagerService
                ->setid($targetGame->id)
                ->setTitle($existingGame->title)
                ->editGameTitle();
        } catch (\Exception $e) {
            DB::rollBack();
            return '标题已存在'; // 确保返回值符合测试预期
        }
    });

    expect($editGameTitleResults)->toBeString();

    // 此时事务状态正常,可正常查询
    $actualGameRecord = Game::where('id', $targetGame->id)->first();
    expect($actualGameRecord->title)->toBe($targetGame->title);
});

注意:RefreshDatabase trait是在每个测试结束后回滚事务,但问题出在测试执行过程中事务已经中止,因此必须在异常发生时及时处理事务状态。

内容的提问来源于stack exchange,提问作者Jheems

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 17:26:03