PHPSpec 行为约定示例

解读

国内一线互联网公司在招聘 PHP 高级工程师时,越来越强调“测试驱动”与“代码可维护性”。PHPSpec 作为 BDD(行为驱动开发)阵营的代表工具,常被用来验证“类是否按约定行为工作”。面试官抛出“请写一个 PHPSpec 行为约定示例”,并不是想听你背文档,而是考察三件事:

  1. 你是否真的用 PHPSpec 写过业务代码,而不是只会 PHPUnit;
  2. 你是否理解“先写约定,再写实现”的 BDD 节奏;
  3. 你能否把日常业务(价格计算、库存扣减、优惠券核销等)抽象成清晰的 Spec 描述。
    回答时务必给出一个“可运行、可落地”的完整示例,并解释每一步在真实项目中的价值。

知识点

  1. BDD 与 TDD 区别:BDD 关注“行为”,TDD 关注“功能”。
  2. PHPSpec 核心概念:
    • Specification 类命名规则 Spec 后缀;
    • let() 依赖注入;
    • it_* / its_* 方法即“行为约定”;
    • 匹配器:shouldBe、shouldReturn、shouldThrow、shouldHaveType 等;
    • 预言对象(Prophecy)自动注入,避免手写 Mock。
  3. Composer 脚本:composer require --dev phpspec/phpspecvendor/bin/phpspec desc 自动生成骨架。
  4. 国内落地场景:电商价格域、营销域、支付域的纯值对象(Value Object)最适合 PHPSpec,因其无外部 IO,行为确定。
  5. 与 PHPUnit 互补:PHPSpec 做“单元行为”,PHPUnit 做“集成 & 回归”,两者并存是主流方案。

答案

下面给出国内电商“满减价格计算器”的完整示例,从命令行到绿灯一步到位,可直接写进简历项目经验。

  1. 安装并生成骨架
composer require --dev phpspec/phpspec
vendor/bin/phpspec desc App/Domain/Price/FullReductionCalculator

PHPSpec 会生成 spec/Domain/Price/FullReductionCalculatorSpec.php

  1. 编写约定(红色阶段)
<?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();
    }
}
  1. 运行 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;
    }
}
  1. 再次运行 vendor/bin/phpspec run,全部绿灯。此时可补充边界值、异常场景,继续重构。

  2. 集成到 CI(GitHub Actions / GitLab CI)

- name: Run PHPSpec
  run: vendor/bin/phpspec run --format=progress

国内阿里云效、腾讯云 Coding 均支持同样脚本。

拓展思考

  1. 若价格计算依赖外部促销服务,如何把“外部调用”转为预言对象?
    let($promotionClient) 中注入接口,用 ->willReturn() 模拟返回,保持 Spec 纯内存。
  2. 当规则复杂到“满 300 减 50 且可与优惠券叠加”时,PHPSpec 是否仍然适用?
    建议将“策略链”拆成多个 Value Object,每个策略独立 Spec,再用组合模式集成,避免一个类职责过重。
  3. 国内高并发场景下,PHPSpec 写出的代码如何保障性能?
    Spec 只保证行为正确,性能需配合 PHPUnit 基准测试与 OPcache 预热;同时把计算逻辑放到 PriceService 层,利用本地缓存或 Redis 预计算,Spec 层无需关心。