本文目录导读:

- 基本事务回滚模式
- 使用事务辅助类
- 使用 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
事务回滚测试的核心原则是:保证测试数据不会影响生产环境,且每个测试用例相互独立,选择哪种方法取决于你的项目架构和框架选择。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播