本文目录导读:

- 基本事务回滚模式
- 使用事务辅助类
- 使用 Laravel 的 RefreshDatabase
- 使用 PHPUnit 的 @transactional 注解
- 使用数据库事务包装器
- 使用 SQL 恢复脚本
- 最佳实践建议
- 选择建议
在 PHP 测试中使用数据库事务回滚是一个很好的实践,可以避免测试数据污染生产数据库,以下是几种常用的方法和最佳实践:
基本事务回滚模式
<?php
class UserRepositoryTest extends TestCase
{
private PDO $pdo;
private PDO $testPdo;
protected function setUp(): void
{
// 创建独立的测试数据库连接
$this->pdo = new PDO(
'mysql:host=localhost;dbname=test_db',
'test_user',
'test_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$this->testPdo = new PDO(
'mysql:host=localhost;dbname=test_db',
'test_user',
'test_password',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
}
public function testCreateUser()
{
// 开始事务
$this->testPdo->beginTransaction();
try {
// 执行测试操作
$userRepository = new UserRepository($this->testPdo);
$result = $userRepository->create([
'name' => 'Test User',
'email' => 'test@example.com'
]);
// 断言结果
$this->assertTrue($result);
$this->assertEquals(1, $this->getUserCount());
// 回滚事务
$this->testPdo->rollBack();
} catch (Exception $e) {
// 发生异常时也要回滚
$this->testPdo->rollBack();
throw $e;
}
}
private function getUserCount(): int
{
$stmt = $this->testPdo->query('SELECT COUNT(*) FROM users');
return (int)$stmt->fetchColumn();
}
}
使用事务辅助类
<?php
trait DatabaseTransactionTrait
{
private PDO $transactionPdo;
protected function beginTransaction(): void
{
if (!$this->transactionPdo->inTransaction()) {
$this->transactionPdo->beginTransaction();
}
}
protected function rollBackTransaction(): void
{
if ($this->transactionPdo->inTransaction()) {
$this->transactionPdo->rollBack();
}
}
protected function runInTransaction(callable $callback): mixed
{
$this->beginTransaction();
try {
$result = $callback();
$this->rollBackTransaction();
return $result;
} catch (Exception $e) {
$this->rollBackTransaction();
throw $e;
}
}
}
class UserRepositoryTest extends TestCase
{
use DatabaseTransactionTrait;
protected function setUp(): void
{
$this->transactionPdo = Database::getTestConnection();
}
public function testCreateUser()
{
$result = $this->runInTransaction(function () {
$userRepository = new UserRepository($this->transactionPdo);
$user = $userRepository->create([
'name' => 'Test User',
'email' => 'test@example.com'
]);
// 验证数据已保存
$this->assertNotNull($user->id);
// 查询验证
$saved = $userRepository->findById($user->id);
$this->assertEquals('Test User', $saved->name);
return $user;
});
// 事务已回滚,验证数据不存在
$userRepository = new UserRepository($this->transactionPdo);
$this->assertNull($userRepository->findById($result->id));
}
}
使用 Laravel 的 RefreshDatabase
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase;
public function testCreateUser()
{
// 使用迁移和种子数据
$this->seed();
$response = $this->post('/api/users', [
'name' => 'Test User',
'email' => 'test@example.com'
]);
$response->assertStatus(201);
// 检查数据库
$this->assertDatabaseHas('users', [
'email' => 'test@example.com'
]);
// RefreshDatabase 会自动回滚事务
}
public function testUpdateUser()
{
$user = User::factory()->create();
$response = $this->put("/api/users/{$user->id}", [
'name' => 'Updated Name'
]);
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'id' => $user->id,
'name' => 'Updated Name'
]);
}
}
使用 PHPUnit 的 @transactional 注解
<?php
use PHPUnit\Framework\TestCase;
class UserPersistenceTest extends TestCase
{
private PDO $pdo;
protected function setUp(): void
{
$this->pdo = new PDO(
'mysql:host=localhost;dbname=test_db',
'test_user',
'test_password'
);
// 开启自动回滚
$this->pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false);
}
protected function tearDown(): void
{
// 回滚所有未提交的事务
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
$this->pdo = null;
}
public function testInsertUser()
{
$this->pdo->beginTransaction();
try {
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute(['Test User', 'test@example.com']);
$count = $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
$this->assertEquals(1, $count);
} finally {
// 总是回滚
$this->pdo->rollBack();
}
}
}
使用数据库事务包装器
<?php
class DatabaseTransactionManager
{
private PDO $connection;
private int $transactionLevel = 0;
public function __construct(PDO $connection)
{
$this->connection = $connection;
}
public function beginTransaction(): void
{
if ($this->transactionLevel === 0) {
$this->connection->beginTransaction();
}
$this->transactionLevel++;
}
public function commit(): void
{
if ($this->transactionLevel === 1) {
$this->connection->commit();
}
$this->transactionLevel--;
}
public function rollBack(): void
{
if ($this->transactionLevel === 1) {
$this->connection->rollBack();
}
$this->transactionLevel--;
}
public function runInTransaction(callable $callback): mixed
{
$this->beginTransaction();
try {
$result = $callback();
$this->commit();
return $result;
} catch (Exception $e) {
$this->rollBack();
throw $e;
}
}
}
// 使用示例
class OrderTest extends TestCase
{
private DatabaseTransactionManager $transactionManager;
private OrderRepository $orderRepository;
protected function setUp(): void
{
$pdo = new PDO('mysql:host=localhost;dbname=test_db', 'test_user', 'test_password');
$this->transactionManager = new DatabaseTransactionManager($pdo);
$this->orderRepository = new OrderRepository($pdo);
}
public function testCreateOrderWithItems()
{
$result = $this->transactionManager->runInTransaction(function () {
// 创建订单
$order = $this->orderRepository->create([
'user_id' => 1,
'status' => 'pending'
]);
// 添加订单项
foreach (['item1', 'item2'] as $item) {
$this->orderRepository->addItem($order->id, $item);
}
// 验证订单完整性
$items = $this->orderRepository->getItems($order->id);
$this->assertCount(2, $items);
return $order;
});
// 事务已回滚,验证数据不存在
$this->assertNull($this->orderRepository->findById($result->id));
}
}
使用 SQL 恢复脚本
<?php
class DatabaseTestTrait
{
protected function cleanupDatabase(): void
{
// 恢复数据库到初始状态
$this->pdo->exec('SET FOREIGN_KEY_CHECKS=0');
$tables = [
'users',
'orders',
'order_items'
];
foreach ($tables as $table) {
$this->pdo->exec("TRUNCATE TABLE {$table}");
}
$this->pdo->exec('SET FOREIGN_KEY_CHECKS=1');
}
protected function setupTestData(): void
{
// 插入测试数据
$this->pdo->exec("
INSERT INTO users (name, email)
VALUES ('Test User', 'test@example.com')
");
}
}
最佳实践建议
使用独立测试数据库
<?php
class TestDatabaseConfig
{
public static function getConnection(): PDO
{
return new PDO(
getenv('TEST_DB_HOST') ?: 'localhost',
getenv('TEST_DB_USER') ?: 'test_user',
getenv('TEST_DB_PASSWORD') ?: 'test_password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
}
}
异常处理
<?php
class TransactionTest extends TestCase
{
public function testTransactionRollbackOnError()
{
$pdo = TestDatabaseConfig::getConnection();
$pdo->beginTransaction();
try {
// 执行一些操作
$pdo->exec("INSERT INTO users (name) VALUES ('Test User')");
// 模拟一个错误
$pdo->exec("INSERT INTO non_existent_table (column) VALUES ('value')");
$this->fail('应该抛出异常');
} catch (PDOException $e) {
$pdo->rollBack();
// 验证回滚成功
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
$this->assertEquals(0, $count);
}
}
}
选择建议
- 简单项目:直接使用 PDO 的
beginTransaction()和rollBack() - Laravel 项目:使用内置的
RefreshDatabasetrait - 复杂项目:实现自定义的事务管理器或使用中间件
- 多测试文件:使用 PHPUnit 的
@transactional注解或自定义 trait
事务回滚测试的核心原则是:保证测试数据不会影响生产环境,且每个测试用例相互独立,选择哪种方法取决于你的项目架构和框架选择。