mcpField
P
phpunit

phpunit

github.com/xobotyi测试与质量23338 次安装今天更新
代码仓库

关于 phpunit

面向 PHP 开发者的 PHPUnit 测试技能,所述规范以 PHPUnit 11+ 和 PHP 8.5+ 为目标。内容侧重测试行为,并涵盖夹具、数据提供器、断言、模拟对象、属性和测试组织。

主要功能

  1. 01以行为为中心的测试命名
  2. 02Arrange-Act-Assert 结构
  3. 03夹具生命周期指导
  4. 04数据提供器模式
  5. 05断言与异常测试
  6. 06Stub、Mock 与 PHP 属性

适用场景

  1. 01创建 PHPUnit 测试类
  2. 02测试异常和数据驱动行为
  3. 03为依赖选择 Stub 或 Mock

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/xobotyi/cc-foundry --skill phpunit

安装后在 Claude Code 中输入/phpunit确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

.dev/
reference-inventory.json
references/
assertions.md
configuration.md
data-providers.md
mocking.md

详细介绍

PHPUnit

测试行为,而不是实现。测试是可执行的文档——如果测试名称无法说明代码的行为,就重写它。

PHPUnit 是 PHP 的标准测试框架。它使用继承 TestCase 的测试用例类,通过 setUp() / tearDown() 设置测试夹具,并提供完整的断言 API。所有模式均适用于 PHP 8.5+ 上的 PHPUnit 11+。仅使用 PHP 8 属性——注解在 11 中已弃用,在 12 中已移除。

参考资料

  • 断言目录、约束、异常预期 → [${CLAUDE_SKILL_DIR}/references/assertions.md] — 按类别整理的完整断言 API、约束系统、自定义断言
  • 测试替身 — 存根、模拟对象、MockBuilder → [${CLAUDE_SKILL_DIR}/references/mocking.md] — createStub 与 createMock 的区别、返回值配置、调用匹配器、参数约束、MockBuilder
  • 数据提供器 — 静态、命名、生成器 → [${CLAUDE_SKILL_DIR}/references/data-providers.md] — #[DataProvider]、#[TestWith]、命名数据集、生成器提供器、外部提供器
  • phpunit.xml 结构、测试套件、源代码配置 → [${CLAUDE_SKILL_DIR}/references/configuration.md] — XML 元素、严格设置、源代码元素、覆盖率报告、执行顺序

测试结构

测试发现与命名
  • 文件: 已配置的测试目录中的 *Test.php。镜像源代码结构:src/Service/PaymentService.php → tests/Unit/Service/PaymentServiceTest.php。
  • 类: final class PaymentServiceTest extends TestCase。始终使用 final。
  • 方法: 使用 test 前缀或 #[Test] 属性。描述行为:使用 testReturnsEmptyCollectionWhenNoResults,而不是 testSearch。
  • 每个生产类对应一个测试类。 拆分到 Unit / Integration 目录中。
Arrange-Act-Assert

将每个测试分为三个阶段:

public function testUserCreationSetsDefaults(): void
{
    // Arrange
    $data = ['name' => 'Alice', 'email' => '[email protected]'];

    // Act
    $user = User::fromArray($data);

    // Assert
    $this->assertSame('Alice', $user->getName());
    $this->assertTrue($user->isActive());
    $this->assertSame([], $user->getRoles());
}
  • 每个测试只执行一次操作。 如果需要执行多次操作,就编写多个测试。
  • 当各阶段显而易见时,注释可选。如果测试较长、各阶段不易立即辨明,则添加注释。
测试粒度
  • 每个测试聚焦一个概念。 如果多个断言验证的是同一行为,则可以使用。行为彼此独立时,应拆分测试。
  • 默认保持快速。 单元测试应在几毫秒内运行。使用分组控制慢速测试:#[Group('slow')]。
  • 必须隔离。 测试不得依赖执行顺序或共享可变状态。每个测试都要自行建立测试环境。

测试夹具

setUp() / tearDown()
  • setUp() 在每个测试方法运行前执行,每次都会使用一个新实例。在此处创建 SUT 及其存根。
  • tearDown() 在每个测试后执行。仅在需要清理外部资源(文件、套接字、数据库连接)时使用。无需清理普通对象。
  • setUpBeforeClass() / tearDownAfterClass() 在每个类中各执行一次。用于开销较大的共享资源(数据库连接)。将资源存储在 static 属性中。
final class PaymentServiceTest extends TestCase
{
    private PaymentService $service;
    private Gateway&Stub $gateway;

    protected function setUp(): void
    {
        $this->gateway = $this->createStub(Gateway::class);
        $this->service = new PaymentService($this->gateway);
    }
}
测试夹具生命周期
  • setUpBeforeClass() — 类作用域;第一个测试前执行一次

  • setUp() — 方法作用域;每个测试前执行

  • assertPreConditions() — 方法作用域;setUp 后、测试前执行

  • assertPostConditions() — 方法作用域;测试后、tearDown 前执行

  • tearDown() — 方法作用域;每个测试后执行

  • tearDownAfterClass() — 类作用域;最后一个测试后执行一次

  • 扩展抽象测试用例时,调用 parent::setUp()——否则会静默跳过父类的测试夹具设置。

  • 需要多个设置方法时,使用 #[Before] / #[After] 属性(避免脆弱的 parent::setUp() 调用链)。

数据提供器

基本用法
use PHPUnit\Framework\Attributes\DataProvider;

#[DataProvider('additionCases')]
public function testAdd(int $a, int $b, int $expected): void
{
    $this->assertSame($expected, $a + $b);
}

public static function additionCases(): array
{
    return [
        'zeros'        => [0, 0, 0],
        'positive sum' => [1, 2, 3],
        'negative'     => [-1, 1, 0],
    ];
}
  • 提供器必须是 public static。 PHPUnit 11 已移除非静态提供器。
  • 始终使用命名数据集——字符串键会生成易读的失败输出。
  • 使用 #[DataProvider] 属性,不要使用 @dataProvider 注解。
内联数据

适用于规模较小、简单的数据集——无需提供器方法:

use PHPUnit\Framework\Attributes\TestWith;

#[TestWith([0, 0, 0])]
#[TestWith([1, 2, 3])]
#[TestWith([-1, 1, 0])]
public function testAdd(int $a, int $b, int $expected): void
{
    $this->assertSame($expected, $a + $b);
}
生成器提供器

适用于规模较大或通过计算生成的数据集:

public static function boundaryCases(): Generator
{
    yield 'min int' => [PHP_INT_MIN, 0, PHP_INT_MIN];
    yield 'max int' => [PHP_INT_MAX, 0, PHP_INT_MAX];
}
数据提供器规则
  • 数据必须是标量或不可变值——数据提供器中不得包含服务对象或复杂对象图。
  • 数据提供器中不得使用模拟对象——执行数据提供器时,框架尚未初始化。
  • PHPUnit 11 禁止使用空数据提供器——否则会抛出 InvalidDataProviderException。
  • 一个测试方法可以叠加多个数据提供器——数据集会合并。

有关外部数据提供器、TestDox 集成和边缘情况,请参阅 ${CLAUDE_SKILL_DIR}/references/data-providers.md。

断言

核心断言
$this->assertSame($expected, $actual);        // Strict === (preferred)
$this->assertEquals($expected, $actual);       // Loose == (use sparingly)
$this->assertTrue($condition);
$this->assertFalse($condition);
$this->assertNull($value);
$this->assertInstanceOf(Expected::class, $obj);
$this->assertCount(3, $collection);
$this->assertEmpty($collection);
$this->assertArrayHasKey('key', $array);
$this->assertContains($needle, $haystack);     // Strict comparison
  • 优先使用 assertSame() 而不是 assertEquals()——严格的类型比较可以发现更多错误。
  • 每个测试中可以包含多个断言,前提是它们验证的是同一行为。
字符串断言
$this->assertStringStartsWith('Error:', $message);
$this->assertStringEndsWith('.php', $filename);
$this->assertStringContainsString('needle', $haystack);
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}$/', $date);
浮点数比较
$this->assertEqualsWithDelta(3.14, $result, 0.01);
异常测试
public function testThrowsOnInvalidInput(): void
{
    $this->expectException(InvalidArgumentException::class);
    $this->expectExceptionMessage('must be positive');

    $calculator->divide(1, 0);
}
  • 在会抛出异常的代码之前调用 expectException()——它会设置预期。
  • 异常类型较宽泛时,使用 expectExceptionMessage()——验证消息中包含指定子字符串。
  • 使用 expectExceptionMessageMatches() 进行正则表达式匹配。
弃用 / 错误预期
public function testTriggersDeprecation(): void
{
    $this->expectUserDeprecationMessage('use newMethod() instead');

    $service->oldMethod();
}

完整的断言目录、约束系统和格式字符串断言,请参阅 ${CLAUDE_SKILL_DIR}/references/assertions.md。

模拟

存根与模拟对象
  • 存根(createStub())——控制返回值,不验证调用。
  • 模拟对象(createMock())——验证交互(是否调用方法、参数是否匹配)。

默认使用存根。只有在需要验证副作用是否发生时才使用模拟对象。

创建存根
$repo = $this->createStub(UserRepository::class);
$repo->method('find')->willReturn(new User(name: 'Alice'));

$service = new UserService($repo);
$result = $service->getUser(1);

$this->assertSame('Alice', $result->name);

多个方法的简写:

$repo = $this->createConfiguredStub(UserRepository::class, [
    'find'   => new User(name: 'Alice'),
    'exists' => true,
]);
创建模拟对象
$logger = $this->createMock(Logger::class);
$logger->expects($this->once())
    ->method('error')
    ->with($this->stringContains('payment failed'));

$service = new PaymentService($logger);
$service->process($invalidPayment);
返回值配置
$stub->method('fetch')->willReturn('value');              // Fixed value
$stub->method('fetch')->willReturn('a', 'b', 'c');       // Consecutive values
$stub->method('fetch')->willReturnArgument(0);            // Return first arg
$stub->method('fetch')->willReturnSelf();                 // Fluent interface
$stub->method('fetch')->willReturnCallback(fn ($id) => "item-{$id}");
$stub->method('fetch')->willThrowException(new RuntimeException('fail'));
$stub->method('fetch')->willReturnMap([
    ['key1', 'value1'],
    ['key2', 'value2'],
]);
模拟规则
  • 在边界处使用模拟。 模拟外部服务、数据库、文件系统、时钟,不要模拟内部函数。
  • 当有伪实现或内存实现可用时,不要模拟自己编写的代码。
  • 优先使用依赖注入,而不是复杂的模拟设置。 将协作者作为构造函数参数传入,并在测试中为其设置存根。
  • 绝不要模拟正在测试的对象。 如果需要模拟 SUT 的一部分,说明 SUT 承担了过多职责——应将其拆分。
  • 测试替身优先使用接口而非类——限制更少,设计也更好。
  • 不要对存根调用 expects()——该方法在 11 中已弃用,在 12 中会报错。

有关 MockBuilder、交叉类型、调用匹配器和 PHP 8.4 属性钩子的更多信息,请参阅 ${CLAUDE_SKILL_DIR}/references/mocking.md。

属性

PHPUnit 11 仅使用 PHP 8 属性。所有属性均位于 PHPUnit\Framework\Attributes 命名空间中。

测试元数据
  • #[Test]——将非 test* 方法标记为测试
  • #[DataProvider('method')]——关联数据提供器
  • #[DataProviderExternal(Class::class, 'method')]——关联外部数据提供器
  • #[TestWith([args])]——内联数据提供器
  • #[TestDox('description')]——自定义 TestDox 描述
  • #[Depends('testMethod')]——声明测试依赖项
  • #[Group('name')]——分配到指定组
  • #[Ticket('PROJ-123')]——关联问题跟踪器中的问题
跳过 / 条件判断
  • #[RequiresPhp('>= 8.4')]——PHP 版本不匹配时跳过
  • #[RequiresPhpExtension('pdo_pgsql')]——缺少扩展时跳过
  • #[RequiresOperatingSystemFamily('Linux')]——在其他操作系统上跳过
  • #[RequiresFunction('sodium_crypto_sign')]——缺少函数时跳过
  • #[RequiresMethod(PDO::class, 'sqliteCreateFunction')]——缺少方法时跳过
覆盖率
  • #[CoversClass(ClassName::class)]——测试覆盖此类
  • #[CoversFunction('functionName')]——测试覆盖此函数
  • #[CoversMethod(ClassName::class, 'method')]——测试覆盖此方法
  • #[CoversNothing]——测试不计入覆盖率(集成测试)
  • #[UsesClass(ClassName::class)]——允许使用但不计入覆盖率的依赖项
  • #[UsesFunction('functionName')]——允许使用但不计入覆盖率的函数
Fixture
  • #[Before] — 在每项测试前运行方法(setUp 的替代方案)
  • #[After] — 在每项测试后运行方法(tearDown 的替代方案)
  • #[BeforeClass] — 在首项测试前运行静态方法
  • #[AfterClass] — 在最后一项测试后运行静态方法
  • #[BackupGlobals(true)] — 为此测试备份/恢复全局变量
  • #[BackupStaticProperties(true)] — 备份/恢复静态属性
测试行为
  • #[DoesNotPerformAssertions] — 抑制有风险测试警告
  • #[RunInSeparateProcess] — 在独立的 PHP 进程中隔离运行
  • #[RunTestsInSeparateProcesses] — 类中的所有测试均在独立进程中隔离运行
  • #[Small] / #[Medium] / #[Large] — 执行时间限制(1s/10s/60s)

测试组织

目录结构
tests/
├── Unit/                     # Fast, isolated, no I/O
│   ├── Service/
│   │   └── PaymentServiceTest.php
│   └── Model/
│       └── UserTest.php
├── Integration/              # Real dependencies, slower
│   └── Repository/
│       └── UserRepositoryTest.php
└── bootstrap.php             # Autoloader for tests
  • 在 tests/Unit/ 和 tests/Integration/ 下镜像源目录结构。
  • 单元测试 — 不使用数据库、文件系统或网络。模拟所有边界。
  • 集成测试 — 使用真实依赖项。使用 #[CoversNothing] 标记,避免污染覆盖率指标。
测试套件

在 phpunit.xml 中定义,以便选择性执行:

<testsuites>
    <testsuite name="unit">
        <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="integration">
        <directory>tests/Integration</directory>
    </testsuite>
</testsuites>

运行子集:phpunit --testsuite unit、phpunit --group slow。

配置

推荐的 phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.5/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         cacheDirectory=".phpunit.cache"
         executionOrder="depends,random"
         beStrictAboutTestsThatDoNotTestAnything="true"
         beStrictAboutOutputDuringTests="true"
         failOnWarning="true"
         failOnRisky="true"
         failOnDeprecation="true"
         failOnNotice="true">
    <testsuites>
        <testsuite name="unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>
    <source restrictDeprecations="true"
            restrictNotices="true"
            restrictWarnings="true">
        <include>
            <directory suffix=".php">src</directory>
        </include>
    </source>
</phpunit>
关键配置选项
  • executionOrder="depends,random" — 随机调整测试顺序以发现隐藏依赖,同时遵循显式的 #[Depends]。
  • beStrictAboutTestsThatDoNotTestAnything="true" — 将没有断言的测试标记为有风险。
  • failOnDeprecation="true" — 尽早发现代码中的弃用问题。
  • <source> 与 restrictDeprecations — 仅显示代码中的问题,而非供应商依赖项中的问题。
  • cacheDirectory — 将 .phpunit.cache 添加到 .gitignore。
代码覆盖率

需要 PCOV 或 Xdebug 扩展:

phpunit --coverage-html build/coverage --coverage-clover build/clover.xml

使用 #[CoversClass] 和 #[UsesClass] 属性精确指定覆盖目标。启用 beStrictAboutCoverageMetadata="true" 后,不带覆盖率属性的测试会被视为有风险。

完整的 XML 参考、覆盖率报告类型和执行顺序选项,请参阅 ${CLAUDE_SKILL_DIR}/references/configuration.md。

应用

编写测试时:默默应用所有约定,不要逐条讲述遵循了哪些规则。遵循项目现有的测试风格。如果现有代码库与某项约定冲突,请遵循代码库,并指出一次差异。

审查测试时:指出具体问题,并直接展示修复方式。不要说教——说明问题所在以及如何修复。

Bad:  "According to PHPUnit best practices, you should use createStub
       instead of createMock when you don't need expectations..."
Good: "createMock → createStub (no expects() call, stub is sufficient)"

集成

php 技能决定语言选择;本技能决定 PHPUnit 测试相关决策。coding 技能决定工作流(探索、规划、验证)。

测试行为,而非实现。拿不准时,少用模拟。

评论

支持 Markdown