PHPSpec 行为约定示例
解读
国内一线互联网公司在招聘 PHP 高级工程师时,越来越强调“测试驱动”与“代码可维护性”。PHPSpec 作为 BDD(行为驱动开发)阵营的代表工具,常被用来验证“类是否按约定行为工作”。面试官抛出“请写一个 PHPSpec 行为约定示例”,并不是想听你背文档,而是考察三件事:
- 你是否真的用 PHPSpec 写过业务代码,而不是只会 PHPUnit;
- 你是否理解“先写约定,再写实现”的 BDD 节奏;
- 你能否把日常业务(价格计算、库存扣减、优惠券核销等)抽象成清晰的 Spec 描述。
回答时务必给出一个“可运行、可落地”的完整示例,并解释每一步在真实项目中的价值。
知识点
- BDD 与 TDD 区别:BDD 关注“行为”,TDD 关注“功能”。
- PHPSpec 核心概念:
- Specification 类命名规则
Spec后缀; let()依赖注入;it_*/its_*方法即“行为约定”;- 匹配器:shouldBe、shouldReturn、shouldThrow、shouldHaveType 等;
- 预言对象(Prophecy)自动注入,避免手写 Mock。
- Specification 类命名规则
- Composer 脚本:
composer require --dev phpspec/phpspec,vendor/bin/phpspec desc自动生成骨架。 - 国内落地场景:电商价格域、营销域、支付域的纯值对象(Value Object)最适合 PHPSpec,因其无外部 IO,行为确定。
- 与 PHPUnit 互补:PHPSpec 做“单元行为”,PHPUnit 做“集成 & 回归”,两者并存是主流方案。
答案
下面给出国内电商“满减价格计算器”的完整示例,从命令行到绿灯一步到位,可直接写进简历项目经验。
- 安装并生成骨架
composer require --dev phpspec/phpspec
vendor/bin/phpspec desc App/Domain/Price/FullReductionCalculator
PHPSpec 会生成 spec/Domain/Price/FullReductionCalculatorSpec.php。
- 编写约定(红色阶段)
<?php
namespace spec\App\Domain\Price;
use App\Domain\Price\FullReductionCalculator;
use PhpSpec\ObjectBehavior;
class FullReductionCalculatorSpec extends ObjectBehavior
{
function it_calculates_final_price_when_over_threshold()
{
// 场景:满 300 减 50
$this->beConstructedWith(30000, 5000); // 单位:分
$this->calculate(35000)->shouldReturn(30000); // 350 - 50 = 300
}
function it_returns_original_price_when_under_threshold()
{
$this->beConstructedWith(30000, 5000);
$this->calculate(29999)->shouldReturn(29999);
}
function it_throws_exception_when_threshold_less_than_reduction()
{
$this->beConstructedWith(30000, 35000);
$this->shouldThrow(\InvalidArgumentException::class)->duringInstantiation();
}
}
- 运行
vendor/bin/phpspec run看到红色失败,进入绿色阶段,实现业务类
<?php
namespace App\Domain\Price;
final class FullReductionCalculator
{
private int $threshold; // 分
private int $reduction; // 分
public function __construct(int $threshold, int $reduction)
{
if ($threshold < $reduction) {
throw new \InvalidArgumentException('Threshold must >= reduction');
}
$this->threshold = $threshold;
$this->reduction = $reduction;
}
public function calculate(int $cents): int
{
return $cents >= $this->threshold ? $cents - $this->reduction : $cents;
}
}
-
再次运行
vendor/bin/phpspec run,全部绿灯。此时可补充边界值、异常场景,继续重构。 -
集成到 CI(GitHub Actions / GitLab CI)
- name: Run PHPSpec
run: vendor/bin/phpspec run --format=progress
国内阿里云效、腾讯云 Coding 均支持同样脚本。
拓展思考
- 若价格计算依赖外部促销服务,如何把“外部调用”转为预言对象?
在let($promotionClient)中注入接口,用->willReturn()模拟返回,保持 Spec 纯内存。 - 当规则复杂到“满 300 减 50 且可与优惠券叠加”时,PHPSpec 是否仍然适用?
建议将“策略链”拆成多个 Value Object,每个策略独立 Spec,再用组合模式集成,避免一个类职责过重。 - 国内高并发场景下,PHPSpec 写出的代码如何保障性能?
Spec 只保证行为正确,性能需配合 PHPUnit 基准测试与 OPcache 预热;同时把计算逻辑放到 PriceService 层,利用本地缓存或 Redis 预计算,Spec 层无需关心。