如何为依赖含DAO的外部对象的PHP代码编写PHPUnit测试及可测试性重构
PHPUnit测试相关问题解决方案
Q1 如何编写需要访问数据库的测试代码?
分两种场景处理:
- 单元测试场景:不要访问真实数据库,将数据库相关依赖全部Mock,返回预设的测试值即可,避免测试依赖外部环境导致不稳定。
- 集成测试场景:可使用独立的测试数据库,测试前加载固定测试数据(Fixture),测试后自动回滚事务清空测试痕迹,PHPUnit可搭配
phpunit/dbunit扩展实现,也可以手动实现事务回滚逻辑,示例如下:
protected $db; public function setUp(): void { // 连接测试环境数据库 $this->db = new PDO('mysql:host=localhost;dbname=test_db', 'test_user', 'test_pass'); $this->db->beginTransaction(); } public function tearDown(): void { // 测试结束回滚事务,避免污染测试库 $this->db->rollBack(); } public function testGetOptionFromDb() { // 插入测试数据 $this->db->exec("INSERT INTO option (id, value) VALUES (1, 'db_test_value')"); $data = ['id' => 1]; $option = Sample_Model_Service_B::getOption($data); $this->assertSame('db_test_value', $option); }
Q2 如何重构现有代码使其更具可测试性?
原代码存在硬依赖静态方法、内部实例化DAO、核心逻辑封装为私有方法等影响可测试性的问题,可按以下规则重构:
- 移除硬编码依赖:使用依赖注入,将ServiceA构造函数中直接调用
Sample_Model_Service_B::getOption的逻辑改为接收外部传入的$options参数,或者传入ServiceB实例。 - 剥离内部实例化逻辑:ServiceB的
getOption方法不要自己实例化Model_Dao_Option,改为接收DAO实例作为参数,方便Mock。 - 私有方法处理:如果私有方法逻辑复杂,可提取为独立的公共服务类,或修改为protected方法,不建议直接测试私有方法,应该通过公共方法的输出覆盖私有方法的逻辑。
- 修复语法问题:原代码属性声明缺少分号,
getter方法中retrn为拼写错误,需要修正为return。
重构后的ServiceA示例:
class Sample_Model_Service_A { private $_result; private $_options; private $_someValue; // 依赖注入options,移除硬依赖 public function __construct($params, $ids, array $options) { $this->_options = $options; } // 新增公共方法暴露计算逻辑 public function computeResult() { $this->setSomeValue(); $this->setResult(); return $this->getter(); } private function setSomeValue() { // 原逻辑不变 $someValue = '处理后的中间值'; $this->_someValue= $someValue; } private function setResult() { // 原逻辑不变 $result = '处理后的最终结果'; $this->_result = $result; } public function getter() { return $this->_result; } }
重构后的测试代码无需反射,写法更简洁:
public function testComputeResult() { $params = []; $ids = []; $options = ['test_key' => 'test_value']; // 直接传入模拟配置 $sample = new Sample_Model_Service_A($params, $ids, $options); $actual = $sample->computeResult(); $this->assertSame('预期结果', $actual); }
Q3 是否存在尽量少改动生产代码即可编写测试的方案?
存在,可通过静态方法Mock+反射的方案实现,生产代码仅需要修复基础语法错误即可,无需修改核心逻辑。
原测试代码的问题在于:构造函数调用的Sample_Model_Service_B::getOption会直接连接数据库,且setResult无返回值,结果存储在私有属性$_result中,不能直接拿invoke的返回值做断言。
最小改动的测试代码示例(搭配Mockery实现静态方法Mock):
/** * @runTestsInSeparateProcesses 避免静态Mock和其他测试冲突 */ public function testSetResult() { // 1. Mock静态方法,绕过数据库调用 \Mockery::mock('alias:Sample_Model_Service_B') ->shouldReceive('getOption') ->andReturn(['test_key' => 'test_value']); // 2. 初始化ServiceA $params = $ids = []; $data = ['id' => 1]; $sample = new Sample_Model_Service_A($params, $ids, $data); // 3. 用反射给依赖的私有属性$_someValue赋值 $reflection = new ReflectionClass($sample); $someValueProp = $reflection->getProperty('_someValue'); $someValueProp->setAccessible(true); $someValueProp->setValue($sample, '测试用的中间值'); // 4. 调用私有方法setResult $method = $reflection->getMethod('setResult'); $method->setAccessible(true); $method->invoke($sample); // 5. 用反射读取私有属性$_result的结果 $resultProp = $reflection->getProperty('_result'); $resultProp->setAccessible(true); $actualResult = $resultProp->getValue($sample); // 6. 断言 $this->assertSame('预期的结果', $actualResult); }
内容的提问来源于stack exchange,提问作者Hodaka Matsuki
相关产品推荐
相关产品推荐

