测试驱动开发工作流
通过红-绿-重构循环驱动设计
测试驱动开发工作流 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。
测试驱动开发是设计工具
测试驱动开发通常被当作一种测试技术来推广,但它真正的价值在于设计压力。先编写测试,会迫使您在编写任何实现之前,就定义对象的公共接口、依赖项和契约。测试只是副产品;更好的设计才是最终产物。
红、绿、重构
这个循环包含三个严格阶段:
- 红——为下一个微小行为编写一个失败的测试。它必须因为正确的原因失败。
- 绿——编写能通过测试的最少代码,即使代码并不漂亮。
- 重构——在保持通过的同时,清理实现和测试。
纪律很重要:绝不要跳过红阶段(否则您可能什么都没测试),也绝不要在绿阶段过度构建。
红:先编写失败的测试
我们将构建一个应用百分比折扣的 PriceCalculator。先为尚不存在的行为编写测试。运行测试应该失败,因为该类尚未定义——这是有效的红阶段。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_applies_a_percentage_discount(): void
{
$calc = new PriceCalculator();
// 100 with 10% off -> 90.0
self::assertSame(90.0, $calc->withDiscount(100.0, 10));
}
}
绿:编写通过测试的最少代码
现在编写能让测试变绿的最小实现。请克制自己,不要添加舍入规则、验证或货币处理——目前还没有失败的测试要求这些功能。
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
return $amount - ($amount * $percent / 100);
}
}
$calc = new PriceCalculator();
var_dump($calc->withDiscount(100.0, 10)); // float(90)
通过三角验证推动泛化
一个测试可以通过硬编码来满足。三角验证——添加第二个不同的示例——会迫使实现进行泛化。请添加一个简单方案无法伪造的情况,从而推动真正的公式出现。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_zero_percent_is_unchanged(): void
{
self::assertSame(50.0, (new PriceCalculator())->withDiscount(50.0, 0));
}
public function test_full_discount_is_free(): void
{
self::assertSame(0.0, (new PriceCalculator())->withDiscount(50.0, 100));
}
}
将边界情况变成规范
在测试驱动开发中,新需求就是一个新的失败测试。假设负折扣无效。请先编写一个期望抛出异常的测试;它会因为目前还没有任何代码抛出异常而失败。这个测试记录了契约。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorValidationTest extends TestCase
{
public function test_rejects_negative_discount(): void
{
$this->expectException(\InvalidArgumentException::class);
(new PriceCalculator())->withDiscount(100.0, -5);
}
}
再次变绿,然后重构
添加保护逻辑,使新测试通过,然后放心重构——现有测试会捕获回归问题。请注意,我们也是在某个测试(或已知需求)证明有必要之后,才引入舍入处理的。
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
if ($percent < 0 || $percent > 100) {
throw new \InvalidArgumentException('percent must be 0..100');
}
return round($amount - ($amount * $percent / 100), 2);
}
}
var_dump((new PriceCalculator())->withDiscount(19.99, 15)); // float(16.99)
数据提供器压缩示例
某个行为稳定后,可以使用数据提供器将许多示例对合并为一个参数化测试。这样既能保持红绿循环快速运行,也能让意图表易于阅读。
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
final class DiscountTableTest extends TestCase
{
#[DataProvider('cases')]
public function test_discounts(float $amount, float $pct, float $expected): void
{
self::assertSame($expected, (new PriceCalculator())->withDiscount($amount, $pct));
}
public static function cases(): array
{
return [
'no discount' => [100.0, 0, 100.0],
'ten percent' => [100.0, 10, 90.0],
'free' => [100.0, 100, 0.0],
];
}
}
保持循环快速
只有当反馈循环以秒计而不是以分钟计时,测试驱动开发才有效。以下是一些实用方法:
- 运行单个文件或使用筛选器:
phpunit --filter test_full_discount_is_free。 - 使用
--testdox,以行为规范的形式阅读测试。 - 让单元测试不包含输入/输出操作——内层循环中不要访问数据库、网络或文件系统。
vendor/bin/phpunit --testdox --filter PriceCalculator测试也要重构
重构阶段覆盖整个测试套件,而不仅仅是生产代码。使用辅助函数和 setUp()消除重复,按照行为命名测试,并删除那些不再断言任何有意义内容的测试。测试是您要永久维护的代码——请用同样的认真态度对待它们。
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calc;
protected function setUp(): void
{
$this->calc = new PriceCalculator(); // shared setup, no duplication
}
public function test_ten_percent(): void
{
self::assertSame(90.0, $this->calc->withDiscount(100.0, 10));
}
}
测试驱动开发塑造依赖关系
由于您先编写测试,笨拙的依赖项会立即暴露出来。如果一个类很难在测试中实例化,这就是设计反馈:请注入协作者,而不是在类内部使用 new创建它们。下面的折扣计算器通过接收舍入策略而不是将其硬编码,从而变得可测试——测试驱动开发促成了这个扩展点。
<?php
interface RoundingPolicy { public function round(float $v): float; }
final class PriceCalculator
{
public function __construct(private RoundingPolicy $rounding) {}
public function withDiscount(float $amount, float $percent): float
{
return $this->rounding->round($amount - ($amount * $percent / 100));
}
}
// Tests inject a deterministic RoundingPolicy; no hidden global behavior.
快速检查
红阶段的目的是什么?
总结
您已经将测试驱动开发作为一种设计规范进行了实践:
- 红—绿—重构:失败的测试、最少的代码,然后在保持通过的同时进行清理。
- 三角验证会迫使实现具备通用性;新需求通过新的失败测试加入。
- 数据提供器会压缩稳定案例;
--filter和--testdox能保持循环快速。 - 测试也要重构——它们是长期存在的代码。
下一步:使用 Mockery 创建灵活的测试替身。
常见问题解答
「测试驱动开发工作流」课时是免费的吗?
是的 — 「测试驱动开发工作流」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。
「测试驱动开发工作流」这节课中我会学到什么?
通过红-绿-重构循环驱动设计 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 PHP Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。
「测试驱动开发工作流」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 PHP Academy 课中编写并运行代码吗?
能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。