php tdd有效实施需满足节奏可控、失败可预期、重构敢动三条件;须本地安装phpunit并正确配置路径与版本;setup/teardown应轻量隔离资源;mock仅限外部依赖;测例命名需明确行为,过期测例应及时删除。

PHP 用 TDD 开发不是“先写测试再写代码”这么简单,关键在于节奏是否可控、失败是否可预期、重构是否敢动——没建立这三个条件,测例很快变成维护负担。
怎么让 phpunit 真正跑起来而不是报错
很多人卡在第一步:执行 phpunit 就提示 Class 'PHPUnit\Framework\TestCase' not found 或 Could not open input file: phpunit。这不是环境没装好,而是没分清“全局安装”和“项目本地安装”的边界。
- 永远优先用 Composer 在项目里装:
composer require --dev phpunit/phpunit - 运行时别敲
phpunit,改用./vendor/bin/phpunit(Linux/macOS)或vendor\bin\phpunit(Windows) - 确保
phpunit.xml存在且<testsuites></testsuites>指向tests/目录,否则它默认找*Test.php却忽略你放错位置的文件 - PHP 版本要匹配:PHPUnit 10 要求 PHP 8.1+,用 PHP 7.4 还硬上会直接 fatal error
setUp() 和 tearDown() 到底该放什么
这两个方法常被滥用成“把所有初始化都塞进去”,结果测例之间开始互相污染,比如数据库连接没关、临时文件没删、静态属性没重置——单个跑通过,批量跑就随机失败。
-
setUp()只做当前测例必需的轻量准备:new 一个对象、mock 一个依赖、设置一个属性值 - 不要在
setUp()里执行数据库写入、文件创建、网络请求;这些应该用setUpBeforeClass()+@beforeClass注解控制,并确保配套清理 -
tearDown()不是摆设:如果setUp()创建了资源,这里必须释放;但别写“保险式清理”,比如无条件删某个目录——可能别的测例还在用 - 更安全的做法是用临时路径:
$this->tempDir = sys_get_temp_dir() . '/test_' . uniqid(); mkdir($this->tempDir);,然后在tearDown()里rrmdir($this->tempDir)
Mock 什么、不 mock 什么:别让测试变成配置游戏
Mock 过度会让测例只验证“你调了哪个方法”,而不是“逻辑是否正确”。常见症状是:改一行业务代码,十几个测例红,全在抱怨 Expected x calls, got y。
- 必须 mock:外部服务(HTTP 客户端、数据库连接、邮件发送器)、时间相关(
time(),date())、随机性来源(rand()) - 不该 mock:你自己的类方法,尤其是纯函数或领域模型——它们该被真实执行,否则测试失去意义
- 谨慎 mock 接口实现:如果
PaymentGatewayInterface有多个实现,测例应针对接口契约,而不是某个具体 mock 行为 - 用
$mock->method('process')->willReturn(true)比->expects($this->once())更聚焦结果,少纠缠调用次数
从“能跑”到“敢改”:TDD 的临界点在哪
当新增一个参数、调整一个 if 条件、拆分一个长方法时,如果测例能立刻告诉你“哪里断了、为什么断、影响范围多大”,才算进入 TDD 的有效区。否则只是“带测试的开发”。
- 每个测例命名要说明行为,不是功能点:
testCalculateTotal_returns_zero_when_items_empty()比testCalculateTotal()有用得多 - 拒绝“万能数据驱动”:用
dataProvider是为了覆盖边界,不是为了把 20 个 case 塞进一个方法——那样失败时根本看不出是哪个输入导致 - 重构阶段别碰测例:红→绿→重构,三步不能跳。重构时测例必须全绿,否则说明逻辑还没稳定,或者测例本身在验证错误的东西
- 最常被忽略的一点:删除过期测例。当某段逻辑被移除后,对应测例若还存在,就会成为“幽灵断言”,未来可能掩盖真实问题
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











