PHP 怎么PHPSpec

wen PHP项目 4

PHPSpec 实战指南:从零开始掌握行为驱动开发(BDD)


目录导读(Table of Contents)

  1. 为什么是 PHPSpec?—— 传统测试的痛点
  2. PHPSpec 核心概念:规格(Specification)与协作者(Collaborators)
  3. 环境搭建与项目初始化(Composer + 配置)
  4. 编写第一个 Spec:从描述行为到生成代码
  5. 深入:对象协作与模拟(Prophecy)
  6. 持续集成与最佳实践(TDD 循环 + 团队协作)
  7. 常见问题 FAQ 与踩坑记录
  8. PHPSpec 与 PHPUnit 如何共存?

为什么是 PHPSpec?—— 传统测试的痛点

很多 PHP 开发者最初接触的测试框架是 PHPUnit,它属于“先写代码,后写测试”的验证型工具,但 PHPSpec 改变这一顺序,它的核心理念是 Specification by Example(通过示例描述规格)。
当你面对一个复杂业务逻辑时,PHPSpec 强迫你从 “这个对象应该做什么” 出发,而不是“这个方法如何实现”,它通过描述类行为来驱动设计,从而让代码更聚焦于单一职责(SRP),传统测试中 80% 的时间花在 Mock 依赖上,而在 PHPSpec 中,通过自动生成桩代码(Stub),让设计更顺畅。

PHP 怎么PHPSpec

PHPSpec 核心概念:规格与协作者

  • 规格(Spec):一个 Spec 类对应一个被测试的类,命名规则为 类名SpecOrderSpec 对应 Order 类。
  • 协作者(Collaborators):指被测类的依赖对象,PHPSpec 内置 Prophecy 预言组件,无需手动编写 createMock(),只需在方法参数中声明类型,PHPSpec 自动注入模拟对象。

环境搭建与项目初始化

composer require --dev phpspec/phpspec
./vendor/bin/phpspec init

运行 phpspec init 后,会在项目根目录生成 phpspec.yml 配置文件,基础配置如下:

suites:
  app_suite:
    namespace: App\
    psr4_prefix: App
    src_path: src
    spec_path: spec

同时创建 spec/ 目录用于存放规格类,src/ 存放被测类,确保 composer.json 中的自动加载(PSR-4)已配置好命名空间 App\

编写第一个 Spec:从描述行为到生成代码

进入 spec/ 目录,创建文件 CalculatorSpec.php

namespace spec\App;
use App\Calculator;
use PhpSpec\ObjectBehavior;
class CalculatorSpec extends ObjectBehavior
{
    function it_can_add_two_numbers()
    {
        $this->add(2, 3)->shouldReturn(5);
    }
}

在命令行运行 ./vendor/bin/phpspec run,PHPSpec 会失败,因为它发现 App\Calculator 不存在,接着自动交互式询问是否生成类文件,输入 yes,它会在 src/ 下创建:

namespace App;
class Calculator
{
    public function add($arg1, $arg2)
    {
        // TODO: write logic here
    }
}

再次运行 phpspec run,仍然会失败,提示“方法返回 null”,此时实现逻辑 return $arg1 + $arg2;,再次运行,绿色通过,这正是 测试驱动设计(TDD) 的节奏:红-绿-重构。

深入:对象协作与模拟(Prophecy)

假设 Order 类依赖 Mailer 服务,要在下订单后发送邮件,在 OrderSpec 中:

function it_sends_email_after_creation(Mailer $mailer)
{
    $this->beConstructedWith($mailer); // 构造函数注入
    $mailer->send('order@example.com', 'New Order')->shouldBeCalled();
    $this->create();
}

无需手动创建 Mailer 实例,PHPSpec 自动传入代理对象。shouldBeCalled() 验证调用,willReturn() 可设置返回值,这种方式让测试意图极其清晰。

持续集成与最佳实践

  • 只测公共接口:不要测私有方法,这意味着你设计必须通过公有方法暴露行为。
  • 每个行为一个示例:用 it_ 前缀描述状态变化,it_rejects_invalid_email
  • 禁止访问内部属性:通过 $this->getXxx() 代替 $this->xxx 断言,确保封装性。
  • 在 CI 中集成:在 GitHub Actions 中,执行 vendor/bin/phpspec run --format=pretty 获取美观输出。

常见问题 FAQ 与踩坑记录

Q1:PHPSpec 和 PHPUnit 能一起用吗?
A:完全可以,PHPSpec 用于单元级别的行为驱动设计,而 PHPUnit 用于集成测试或遗留代码的回归保护,两者互补。

Q2:如何处理静态方法或全局函数?
A:在 Spec 中遇到这类依赖时,建议通过依赖注入包装它们,例如创建一个 Clock 接口抽象 time(),并将 Clock 注入类中。这迫使你写出可测试的代码

Q3:shouldReturnshouldBe 有何区别?
A:shouldReturn 比较返回值,shouldBe 比较对象身份(),对于对象,若想要比较属性值,请实现 __toString() 或使用 shouldBeLike()

Q4:运行时报错“No specifications found”?
A:检查 phpspec.yml 中的 spec_path 是否指向正确目录(默认为 spec),且类命名空间与 namespace spec\ 前缀一致。

Q5:如何跳过(Skip)某些测试?
A:在方法上注解 @throws \PhpSpec\Exception\Example\PendingException,并在方法内调用 throw new PendingException(),即可标记为待实现。

PHPSpec 与 PHPUnit 如何共存?

建议项目矩阵

  • 新项目:核心业务模块用 PHPSpec 规范驱动设计,对外接口(HTTP/CLI)用 PHPUnit 做端到端测试。
  • 遗留系统:先引入 PHPUnit 锁死现有 bug,对于新功能,使用 PHPSpec 逐步重构。

最后给新手的忠告:PHPSpec 最大的价值不是“写测试”,而是通过失败的测试引导你改善类设计,当你纠结于“如何让这个测试通过”,实则是在思考“如何让这个类更易用”,坚持几周后,你会发现自己写的类天然具备低耦合性。

搜索引擎优化建议:本文关键词“PHP PHPSpec”密度控制在 2-3%,且在标题、首段、H2 小标题中出现,自然融入“BDD”“Prophecy”“TDD”等 LSI 词汇,提升语义相关性。

抱歉,评论功能暂时关闭!