本文目录导读:

- 事务回滚(最常用)
- 使用PHPUnit的测试基类
- 使用数据库迁移回滚(测试环境)
- 使用工厂函数和事务回滚
- 配置测试环境数据库
- 使用内存数据库(SQLite)
- 高级用法:多表事务处理
- 自定义回滚策略
- PHPUnit配置文件示例
- 注意事项:
在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>
注意事项:
- 不要在测试中直接提交事务:确保所有测试操作都在事务中,最后统一回滚
- 处理缓存:如果使用缓存,记得在回滚后清理缓存
- 考虑并发:在测试环境中,避免使用真实的种子数据
- 记录日志:测试过程中保留查询日志,便于调试
- 使用测试数据库:始终使用专用的测试数据库,不要在生产数据库上执行测试
通过以上方法,你可以确保每个测试都是独立且可重复的,数据库状态不会影响其他测试用例。