在PHP项目里,PDO是操作数据库的主流扩展,单元测试中如果直接依赖真实数据库,不仅会让测试执行速度变慢,还可能因为环境差异导致测试结果不稳定,因此Mock PDO相关对象是单元测试的常见实践。
手动编写PDO Mock类
手动编写Mock类是最基础的实现方式,我们可以创建一个继承PDO的类,重写需要的方法,返回预设的测试数据,避免真实数据库交互。
实现示例
<?php
// 自定义Mock PDO类
class MockPDO extends PDO {
private $mockResults = [];
public function __construct() {
// 不需要真实连接,空实现构造函数
}
// 重写prepare方法,返回自定义的MockStatement对象
public function prepare($statement, $options = []) {
return new MockPDOStatement($this->mockResults);
}
// 设置预设的查询结果
public function setMockResult($result) {
$this->mockResults = $result;
}
}
// 自定义Mock Statement类
class MockPDOStatement {
private $mockResults;
private $executed = false;
public function __construct($mockResults) {
$this->mockResults = $mockResults;
}
public function execute($params = []) {
$this->executed = true;
return true;
}
public function fetchAll($mode = PDO::FETCH_ASSOC) {
if ($this->executed) {
return $this->mockResults;
}
return [];
}
}
// 业务类示例
class UserRepository {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getUsers() {
$stmt = $this->pdo->prepare("SELECT * FROM users");
$stmt->execute();
return $stmt->fetchAll();
}
}
// 单元测试示例
class UserRepositoryTest extends PHPUnitFrameworkTestCase {
public function testGetUsers() {
$mockPdo = new MockPDO();
$mockPdo->setMockResult([
['id' => 1, 'name' => '张三'],
['id' => 2, 'name' => '李四']
]);
$repo = new UserRepository($mockPdo);
$users = $repo->getUsers();
$this->assertCount(2, $users);
$this->assertEquals('张三', $users[0]['name']);
}
}
<?php
使用PHPUnit内置Mock功能
PHPUnit本身提供了创建Mock对象的能力,不需要手动编写完整的Mock类,可以直接生成PDO和PDOStatement的Mock对象,设置方法的返回值即可。
实现示例
<?php
use PHPUnitFrameworkTestCase;
class UserRepositoryTest extends TestCase {
public function testGetUsersWithPHPUnitMock() {
// 创建PDOStatement的Mock对象
$mockStatement = $this->createMock(PDOStatement::class);
// 设置execute方法返回true
$mockStatement->method('execute')
->willReturn(true);
// 设置fetchAll方法返回预设数据
$mockStatement->method('fetchAll')
->willReturn([
['id' => 1, 'name' => '王五'],
['id' => 2, 'name' => '赵六']
]);
// 创建PDO的Mock对象
$mockPdo = $this->createMock(PDO::class);
// 设置prepare方法返回上面的MockStatement对象
$mockPdo->method('prepare')
->willReturn($mockStatement);
$repo = new UserRepository($mockPdo);
$users = $repo->getUsers();
$this->assertCount(2, $users);
$this->assertEquals('王五', $users[0]['name']);
}
}
<?php
使用第三方Mock库
如果需要更灵活的Mock能力,比如模拟PDO的更多方法、处理复杂的数据库交互场景,可以借助像Mockery这样的第三方Mock库,它的语法更简洁,功能更丰富。
实现示例
<?php
use PHPUnitFrameworkTestCase;
use Mockery;
class UserRepositoryTest extends TestCase {
protected function tearDown(): void {
Mockery::close();
}
public function testGetUsersWithMockery() {
// 创建PDO的Mock对象
$mockPdo = Mockery::mock(PDO::class);
// 创建PDOStatement的Mock对象
$mockStatement = Mockery::mock(PDOStatement::class);
// 设置prepare方法返回MockStatement
$mockPdo->shouldReceive('prepare')
->with('SELECT * FROM users')
->andReturn($mockStatement);
// 设置execute方法返回true
$mockStatement->shouldReceive('execute')
->withNoArgs()
->andReturn(true);
// 设置fetchAll方法返回预设数据
$mockStatement->shouldReceive('fetchAll')
->with(PDO::FETCH_ASSOC)
->andReturn([
['id' => 1, 'name' => '孙七'],
['id' => 2, 'name' => '周八']
]);
$repo = new UserRepository($mockPdo);
$users = $repo->getUsers();
$this->assertCount(2, $users);
$this->assertEquals('孙七', $users[0]['name']);
}
}
<?php
不同方案的选择建议
如果是简单的测试场景,手动编写Mock类足够使用,不需要额外依赖;如果项目已经使用PHPUnit,优先使用内置的Mock功能,减少第三方依赖;如果需要模拟复杂的数据库交互逻辑,比如多次查询、事务操作等,第三方Mock库会更合适,能降低Mock代码的编写复杂度。
无论选择哪种方案,核心目标都是隔离数据库依赖,让单元测试可以快速、稳定地执行,同时保证测试覆盖到业务逻辑的正确性。