为ThinkPHP 5.1遗留项目补全单元测试需四步:①写特征测试记录当前行为而非预期;②用内存SQLite+事务回滚解耦数据库;③在setUp中重置Hook、Config、Cookie防状态污染;④按圈复杂度、调用频次、报错日志、外部依赖筛选高价值测试点,再用think-test-helper生成骨架并填充真实断言。

为ThinkPHP 5.1遗留项目补全单元测试,避免重构时行为漂移、线上功能意外失效。
先锁定核心行为:写特征测试
不要一上来就写“应该怎样”,先记录“现在实际怎样”。遗留代码没有契约,只有事实。
第一步:在tests/下新建LegacyFeatureTest.php,继承TestCase。
第二步:找到被高频调用的控制器方法,例如OrderController@pay,构造真实请求参数(含用户ID、订单号、支付金额),调用$this->post()发起模拟请求。
第三步:断言响应状态码、关键字段值、数据库变更结果——比如$this->assertEquals('success', $response['status']),$this->assertDatabaseHas('orders', ['id' => $order_id, 'status' => 'paid'])。
【必须用真实数据跑通一次再写断言】否则你测的不是业务逻辑,而是空壳返回值。
解耦数据库依赖:用内存SQLite+事务回滚
TP5.1默认连接MySQL,但单元测试不能每次清库重插。用内存数据库替代,快且隔离。
方法一:修改phpunit.xml中DB_CONNECTION为sqlite,DB_DATABASE设为:memory:。
方法二:在TestCase.php的setUp()里强制切换连接:Db::connect(['type' => 'sqlite', 'database' => ':memory:']);。
方法三:更稳妥的做法是,在每个测试方法开头执行Db::transaction(function () { ... });,并在tearDown()里不提交——事务自动回滚,无需手动清理表。
注意:TP5.1的模型事件(如after_insert)在内存SQLite中可能不触发,遇到时需临时改用Mock对象拦截模型方法。
绕过全局状态污染:重置App与Config
遗留TP5.1项目常在控制器里直接调用config('app.debug')或Cookie::get(),导致测试间状态残留。
在TestCase.php的setUp()末尾插入:
app('hook')->clear();
think\facade\Config::reset();
think\facade\Cookie::clear();
这三行能切断大部分因配置/钩子/会话缓存引发的测试干扰。不加的话,第二个测试可能读到第一个测试写入的cookie值,结果不可预测。
定位高价值测试点:按收益成本象限筛选
别给所有方法写测试——优先覆盖那些改了容易炸、又没人敢动的代码。
① 打开app/目录,用phploc统计圈复杂度:phploc --exclude vendor app/。
② 筛出圈复杂度 ≥ 12 且被至少3个控制器调用的模型方法,例如OrderModel::calculateDiscount()。
③ 检查日志系统中近30天报错最多的5个路由,对应控制器方法就是必测项。
④ 对接支付、发短信、扣库存等涉及外部服务的方法,必须用$this->mockConsoleOutput()和Mockery::mock()替换真实调用,否则测试会超时或失败。
这四步做完,你手上就有8~12个真正影响业务稳定性的测试目标,而不是盲目覆盖100+个getter/setter。
生成基础测试骨架:用tp-test-helper一键注入
手动写testIndextestCreate太慢。用社区工具快速生成可运行框架。
执行:composer require --dev topthink/think-test-helper。
运行命令:php think test:make app/controller/UserController.php。
它会生成含testIndextestReadtestUpdate的测试类,每个方法都已预置$this->get()$this->post()调用模板和基本断言占位符。
删掉不用的测试方法,把占位符里的assertNotNull替换成你从特征测试里抄来的实际断言值——5分钟就能跑通第一个真实用例。


















