在现代 PHP 开发中,代码质量不再是“能跑就行”的底线要求,而是团队协作、长期维护和持续交付的核心保障。单元测试作为质量保障的第一道防线,能够帮助开发者在早期发现缺陷、重构代码时保持信心,并驱动出更清晰的接口设计。而 PHPUnit 作为 PHP 生态中最成熟的测试框架,几乎是每个 PHP 项目不可或缺的工具。本文将带你从零开始掌握 PHPUnit 的核心用法,并通过一个实战案例演示测试驱动开发(TDD)的完整流程。
为什么需要单元测试
很多开发者对单元测试的抵触来源于“写测试太耗时”的直觉。但真实情况是:没有测试的代码,修改成本会随时间指数级上升。当你接手一个没有测试的遗留系统时,任何一行改动都可能引发不可预知的连锁反应。单元测试带来的核心价值包括:
- 快速反馈:几秒内验证代码逻辑是否正确,无需手动打开浏览器或调用接口。
- 安全重构:有测试覆盖的代码可以放心调整内部实现,只要测试通过,行为就没有改变。
- 活文档:测试用例本身就是对函数行为的精确描述,比注释更可靠。
- 驱动设计:先写测试会迫使你从调用者角度思考接口,从而产生更解耦、更易用的代码。
安装与初始化 PHPUnit
在项目中使用 Composer 安装 PHPUnit(以 PHPUnit 10 为例):
composer require --dev phpunit/phpunit ^10.0
初始化配置文件:
./vendor/bin/phpunit --generate-configuration
生成的 phpunit.xml 中需要配置测试目录、自动加载和覆盖率白名单。一个典型的配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="Application Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
PHPUnit 核心概念速览
在进入实战之前,先快速了解几个关键概念:
- TestCase:所有测试类都继承自
PHPUnit\Framework\TestCase。 - 断言(Assertion):如
assertSame、assertTrue、assertCount,用于验证实际结果是否符合预期。 - 测试方法:以
test开头或使用#[Test]注解的公有方法。 - 数据提供器(Data Provider):用同一套逻辑测试多组输入,避免重复代码。
- Mock 对象:模拟外部依赖,让测试只关注当前单元的逻辑。
实战:用 TDD 开发一个购物车折扣计算器
假设我们需要实现一个类 DiscountCalculator,规则是:购物车总金额满 200 元打 9 折,满 500 元打 8 折,不满 200 元不打折。
第一步:先写测试(红)
在 tests/DiscountCalculatorTest.php 中编写第一个测试:
<?php
use PHPUnit\Framework\TestCase;
use App\DiscountCalculator;
class DiscountCalculatorTest extends TestCase
{
public function testNoDiscountBelow200(): void
{
$calculator = new DiscountCalculator();
$this->assertSame(150.0, $calculator->calculate(150.0));
}
}
运行 ./vendor/bin/phpunit,测试失败,因为 DiscountCalculator 类还不存在。这就是 TDD 的“红”阶段——先让测试失败,明确目标。
第二步:编写最小实现(绿)
创建 src/DiscountCalculator.php:
<?php
namespace App;
class DiscountCalculator
{
public function calculate(float $total): float
{
return $total;
}
}
再次运行测试,通过。此时不要急于添加更多逻辑,先让当前测试通过即可。
第三步:补充更多测试用例
继续添加测试,覆盖折扣边界:
public function testDiscountAt200(): void
{
$calculator = new DiscountCalculator();
$this->assertSame(180.0, $calculator->calculate(200.0));
}
public function testDiscountAt500(): void
{
$calculator = new DiscountCalculator();
$this->assertSame(400.0, $calculator->calculate(500.0));
}
运行测试,两个新用例失败。现在修改实现:
public function calculate(float $total): float
{
if ($total >= 500) {
return $total * 0.8;
}
if ($total >= 200) {
return $total * 0.9;
}
return $total;
}
所有测试通过,进入“重构”阶段。此时可以提取常量、消除魔法数字,而测试会保证行为不变。
第四步:使用数据提供器优化测试
当测试用例增多时,可以用数据提供器让代码更简洁:
/**
* @dataProvider discountProvider
*/
public function testCalculateDiscount(float $input, float $expected): void
{
$calculator = new DiscountCalculator();
$this->assertSame($expected, $calculator->calculate($input));
}
public static function discountProvider(): array
{
return [
'below 200' => [150.0, 150.0],
'exactly 200' => [200.0, 180.0],
'between 200 and 500' => [300.0, 270.0],
'exactly 500' => [500.0, 400.0],
'above 500' => [1000.0, 800.0],
];
}
使用 Mock 隔离外部依赖
单元测试的核心是“单元”,当被测代码依赖数据库、HTTP 客户端或第三方服务时,应使用 Mock 替换真实依赖。例如,一个 OrderService 依赖 PaymentGateway:
public function testOrderIsPaid(): void
{
$gateway = $this->createMock(PaymentGateway::class);
$gateway->expects($this->once())
->method('charge')
->with(100.0)
->willReturn(true);
$service = new OrderService($gateway);
$this->assertTrue($service->pay(100.0));
}
这样测试不依赖真实支付接口,运行速度快且结果稳定。
测试驱动开发的节奏与建议
TDD 的经典循环是 红 → 绿 → 重构:
- 红:写一个失败的测试,明确需求。
- 绿:用最少的代码让测试通过,不追求完美。
- 重构:在测试保护下优化代码结构。
实践中需要注意几点:
- 小步前进:每次只添加一个测试,避免一次性写大量测试后陷入调试泥潭。
- 测试命名清晰:方法名应描述行为,如
testNoDiscountBelow200,而不是testCalculate1。 - 避免测试实现细节:测试应关注输入输出,而不是内部调用了哪个私有方法。
- 保持测试快速:单个测试应在毫秒级完成,否则开发者会不愿意频繁运行。
持续集成中的 PHPUnit
将 PHPUnit 接入 CI 流程是保证测试不被忽略的关键。在 GitHub Actions 中添加:
- name: Run tests
run: ./vendor/bin/phpunit --coverage-text
配合 --coverage-text 可以直观看到覆盖率,但不要盲目追求 100% 覆盖率——覆盖关键业务逻辑和边界条件比覆盖所有 getter/setter 更有价值。
总结
PHPUnit 不仅是一个测试工具,更是一种推动代码设计改进的思维方式。通过本文的实战,你应当已经掌握了 PHPUnit 的基本用法、Mock 技巧以及 TDD 的完整节奏。开始行动的最佳时机就是现在:为你的下一个函数先写一个测试,感受“红绿重构”带来的确定性与掌控感。随着测试套件的增长,你会发现自己对代码的每一次修改都更加从容,而这正是专业开发者与普通编码者之间的分水岭。
未经允许不得转载:任鹏个人博客 » 使用 PHPUnit 进行单元测试与测试驱动开发实战


朋友圈点赞图在线生成源码