You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为依赖含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、核心逻辑封装为私有方法等影响可测试性的问题,可按以下规则重构:

  1. 移除硬编码依赖:使用依赖注入,将ServiceA构造函数中直接调用Sample_Model_Service_B::getOption的逻辑改为接收外部传入的$options参数,或者传入ServiceB实例。
  2. 剥离内部实例化逻辑:ServiceB的getOption方法不要自己实例化Model_Dao_Option,改为接收DAO实例作为参数,方便Mock。
  3. 私有方法处理:如果私有方法逻辑复杂,可提取为独立的公共服务类,或修改为protected方法,不建议直接测试私有方法,应该通过公共方法的输出覆盖私有方法的逻辑。
  4. 修复语法问题:原代码属性声明缺少分号,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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 21:24:02