0Pricing
PHP Academy · 课时

测试驱动开发工作流

通过红-绿-重构循环驱动设计

测试驱动开发工作流 是 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 测试驱动开发工作流
  2. 使用 Mockery 进行模拟与存根
  3. 集成测试与功能测试
  4. 使用 Infection 进行变异测试
← 返回 PHP Academy