PHP 测试用数据库事务回滚

wen PHP项目 2

本文目录导读:

PHP 测试用数据库事务回滚

  1. 基本事务回滚模式
  2. 使用事务辅助类
  3. 使用 Laravel 的 RefreshDatabase
  4. 使用 PHPUnit 的 @transactional 注解
  5. 使用数据库事务包装器
  6. 使用 SQL 恢复脚本
  7. 最佳实践建议
  8. 选择建议

在 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);
        }
    }
}

选择建议

  1. 简单项目:直接使用 PDO 的 beginTransaction()rollBack()
  2. Laravel 项目:使用内置的 RefreshDatabase trait
  3. 复杂项目:实现自定义的事务管理器或使用中间件
  4. 多测试文件:使用 PHPUnit 的 @transactional 注解或自定义 trait

事务回滚测试的核心原则是:保证测试数据不会影响生产环境,且每个测试用例相互独立,选择哪种方法取决于你的项目架构和框架选择。

抱歉,评论功能暂时关闭!