ThinkPHP项目数据库测试回滚

wen PHP项目 3

本文目录导读:

ThinkPHP项目数据库测试回滚

  1. 事务回滚(最常用)
  2. 使用PHPUnit的测试基类
  3. 使用数据库迁移回滚(测试环境)
  4. 使用工厂函数和事务回滚
  5. 配置测试环境数据库
  6. 使用内存数据库(SQLite)
  7. 高级用法:多表事务处理
  8. 自定义回滚策略
  9. PHPUnit配置文件示例
  10. 注意事项:

在ThinkPHP项目中进行数据库测试时,回滚操作是确保测试隔离性的关键,以下是几种常见的回滚策略和实现方法:

事务回滚(最常用)

<?php
namespace app\common\trait;
use think\facade\Db;
trait DatabaseTransaction
{
    protected $connections = [];
    protected function setUp(): void
    {
        parent::setUp();
        // 开始事务(可以处理多个数据库连接)
        $connections = [
            'mysql',      // 默认连接
            // 'mysql2',   // 其他连接
        ];
        foreach ($connections as $connection) {
            Db::connect($connection)->startTrans();
            $this->connections[] = $connection;
        }
    }
    protected function tearDown(): void
    {
        // 回滚事务
        foreach ($this->connections as $connection) {
            Db::connect($connection)->rollback();
        }
        parent::tearDown();
    }
}

使用PHPUnit的测试基类

<?php
namespace tests;
use PHPUnit\Framework\TestCase;
use think\facade\Db;
abstract class DatabaseTestCase extends TestCase
{
    protected $callback;
    protected function setUp(): void
    {
        parent::setUp();
        // 备份数据库状态
        $this->beginTransaction();
    }
    protected function tearDown(): void
    {
        // 回滚数据库
        $this->rollbackTransaction();
        parent::tearDown();
    }
    protected function beginTransaction()
    {
        Db::startTrans();
    }
    protected function rollbackTransaction()
    {
        Db::rollback();
    }
    /**
     * 刷新数据库(清空表数据)
     */
    protected function refreshDatabase()
    {
        // 可选:清空特定表或重建表结构
        $tables = ['users', 'orders', 'products'];
        foreach ($tables as $table) {
            Db::name($table)->delete(true);
        }
    }
}

使用数据库迁移回滚(测试环境)

<?php
namespace tests;
class MigrationTestCase extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        // 运行迁移
        $this->artisan('migrate:fresh --seed');
        // 或只运行迁移
        $this->artisan('migrate');
    }
    protected function tearDown(): void
    {
        // 回滚所有迁移
        $this->artisan('migrate:rollback');
        parent::tearDown();
    }
}

使用工厂函数和事务回滚

<?php
namespace tests\Feature;
use app\common\trait\DatabaseTransaction;
use app\models\User;
use PHPUnit\Framework\TestCase;
class UserTest extends TestCase
{
    use DatabaseTransaction;
    public function testCreateUser()
    {
        // 创建用户(自动事务回滚)
        $user = User::create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => bcrypt('secret')
        ]);
        // 断言
        $this->assertDatabaseHas('users', [
            'email' => 'john@example.com'
        ]);
        // 无需手动清理,tearDown自动回滚
    }
    public function testUpdateUser()
    {
        $user = User::find(1);
        $user->name = 'Jane Doe';
        $user->save();
        $this->assertEquals('Jane Doe', $user->name);
        // 数据会在tearDown中回滚
    }
}

配置测试环境数据库

// config/database.php
return [
    // 测试环境配置
    'connections' => [
        'mysql' => [
            'type'     => 'mysql',
            'hostname' => '127.0.0.1',
            'database' => 'test_database',
            'username' => 'root',
            'password' => 'secret',
            'prefix'   => '',
            // 其他配置...
        ],
        'sqlite' => [
            'type'   => 'sqlite',
            'database' => ':memory:',
            // SQLite内存数据库,自动回滚
        ],
    ],
    // 测试时默认使用
    'default' => env('DB_CONNECTION', 'mysql'),
    // 测试配置
    'test' => [
        'type'     => 'mysql',
        'hostname' => '127.0.0.1',
        'database' => 'test_database',
        'username' => 'root',
        'password' => 'secret',
        'prefix'   => '',
    ],
];

使用内存数据库(SQLite)

<?php
namespace tests;
use PDO;
use PHPUnit\Framework\TestCase;
class InMemoryDatabaseTest extends TestCase
{
    protected $db;
    protected function setUp(): void
    {
        // 创建内存数据库
        $this->db = new PDO('sqlite::memory:');
        // 创建表结构
        $this->db->exec("
            CREATE TABLE users (
                id INTEGER PRIMARY KEY,
                name VARCHAR(255),
                email VARCHAR(255)
            )
        ");
    }
    protected function tearDown(): void
    {
        // 关闭连接,自动清理
        $this->db = null;
    }
    public function testInsert()
    {
        $stmt = $this->db->prepare("INSERT INTO users (name, email) VALUES (?, ?)");
        $stmt->execute(['John', 'john@example.com']);
        $this->assertEquals(1, $this->db->query("SELECT COUNT(*) FROM users")->fetchColumn());
    }
}

高级用法:多表事务处理

<?php
namespace tests\Feature;
use think\facade\Db;
use PHPUnit\Framework\TestCase;
class AdvancedTransactionTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        // 为每个数据库连接启动事务
        Db::connect('mysql')->startTrans();
        Db::connect('postgresql')->startTrans();
        // 可以添加Redis或其他存储的事务
        // Redis::multi();
    }
    protected function tearDown(): void
    {
        // 回滚所有连接
        Db::connect('mysql')->rollback();
        Db::connect('postgresql')->rollback();
        // 回滚Redis
        // Redis::discard();
        parent::tearDown();
    }
    public function testMultiDatabaseOperation()
    {
        // 在多个数据库中执行操作
        Db::connect('mysql')->table('users')->insert(['name' => 'John']);
        Db::connect('postgresql')->table('orders')->insert(['user_id' => 1]);
        // 所有操作都会在tearDown中回滚
    }
}

自定义回滚策略

<?php
namespace app\common\trait;
trait CustomRollback
{
    protected $tablesToSnapshot = [];
    protected function snapshotTables(array $tables)
    {
        $this->tablesToSnapshot = $tables;
        // 保存表数据到临时表
        foreach ($tables as $table) {
            $this->createSnapshot($table);
        }
    }
    protected function createSnapshot($table)
    {
        // 创建临时表并复制数据
        $tempTable = "{$table}_snapshot";
        Db::execute("CREATE TABLE {$tempTable} AS SELECT * FROM {$table}");
    }
    protected function restoreSnapshot()
    {
        foreach ($this->tablesToSnapshot as $table) {
            $tempTable = "{$table}_snapshot";
            // 清空原表
            Db::table($table)->delete(true);
            // 从临时表恢复数据
            Db::execute("INSERT INTO {$table} SELECT * FROM {$tempTable}");
            // 删除临时表
            Db::execute("DROP TABLE {$tempTable}");
        }
    }
    protected function tearDown(): void
    {
        $this->restoreSnapshot();
        parent::tearDown();
    }
}

PHPUnit配置文件示例

<!-- phpunit.xml -->
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Feature">
            <directory>./tests/Feature</directory>
        </testsuite>
        <testsuite name="Unit">
            <directory>./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="testing"/>
        <env name="DB_DATABASE" value="test_database"/>
    </php>
</phpunit>

注意事项:

  1. 不要在测试中直接提交事务:确保所有测试操作都在事务中,最后统一回滚
  2. 处理缓存:如果使用缓存,记得在回滚后清理缓存
  3. 考虑并发:在测试环境中,避免使用真实的种子数据
  4. 记录日志:测试过程中保留查询日志,便于调试
  5. 使用测试数据库:始终使用专用的测试数据库,不要在生产数据库上执行测试

通过以上方法,你可以确保每个测试都是独立且可重复的,数据库状态不会影响其他测试用例。

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