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

