phpunit

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

PHPUnit

PHPUnit

Test behavior, not implementation. Tests are executable documentation — if the test name doesn't explain what the code does, rewrite it.
PHPUnit is PHP's standard testing framework. It uses test case classes extending
TestCase
,
setUp()
/
tearDown()
for fixtures, and a full assertion API. All patterns target PHPUnit 11+ on PHP 8.5+. Use PHP 8 attributes exclusively — annotations are deprecated in 11, removed in 12.
测试行为,而非实现。测试是可执行的文档——如果测试名称无法说明代码的功能,请重写它。
PHPUnit是PHP的标准测试框架。它使用继承
TestCase
的测试用例类,通过
setUp()
/
tearDown()
来管理测试夹具,并提供完整的断言API。所有模式均针对PHP 8.5+环境下的PHPUnit 11+版本。请仅使用PHP 8属性——注解在PHPUnit 11中已被弃用,在12中已被移除。

References

参考资料

  • Assertion catalog, constraints, exception expectations → [
    ${CLAUDE_SKILL_DIR}/references/assertions.md
    ] — Full assertion API grouped by category, constraint system, custom assertions
  • Test doubles — stubs, mocks, MockBuilder → [
    ${CLAUDE_SKILL_DIR}/references/mocking.md
    ] — createStub vs createMock, return config, invocation matchers, argument constraints, MockBuilder
  • Data providers — static, named, generators → [
    ${CLAUDE_SKILL_DIR}/references/data-providers.md
    ] — #[DataProvider], #[TestWith], named datasets, generator providers, external providers
  • phpunit.xml structure, test suites, source config → [
    ${CLAUDE_SKILL_DIR}/references/configuration.md
    ] — XML elements, strict settings, source element, coverage reports, execution order
  • 断言目录、约束、异常预期 → [
    ${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 Structure

测试结构

Discovery and Naming

发现规则与命名规范

  • Files:
    *Test.php
    in configured test directories. Mirror source structure:
    src/Service/PaymentService.php
    tests/Unit/Service/PaymentServiceTest.php
    .
  • Classes:
    final class PaymentServiceTest extends TestCase
    . Always
    final
    .
  • Methods:
    test
    prefix or
    #[Test]
    attribute. Describe the behavior:
    testReturnsEmptyCollectionWhenNoResults
    not
    testSearch
    .
  • One test class per production class. Split into Unit/Integration directories.
  • 文件命名:配置的测试目录下的
    *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

准备-执行-断言(Arrange-Act-Assert)

Structure every test in three phases:
php
public function testUserCreationSetsDefaults(): void
{
    // Arrange
    $data = ['name' => 'Alice', 'email' => 'alice@example.com'];

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

    // Assert
    $this->assertSame('Alice', $user->getName());
    $this->assertTrue($user->isActive());
    $this->assertSame([], $user->getRoles());
}
  • One act per test. If you need multiple acts, write multiple tests.
  • Comments optional when phases are obvious. Add them when the test is long enough that phases aren't immediately clear.
每个测试都分为三个阶段:
php
public function testUserCreationSetsDefaults(): void
{
    // Arrange(准备)
    $data = ['name' => 'Alice', 'email' => 'alice@example.com'];

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

    // Assert(断言)
    $this->assertSame('Alice', $user->getName());
    $this->assertTrue($user->isActive());
    $this->assertSame([], $user->getRoles());
}
  • 每个测试仅一个执行步骤。如果需要多个执行步骤,请编写多个测试。
  • 当阶段划分明显时可省略注释。当测试足够长导致阶段不清晰时,添加注释。

Test Granularity

测试粒度

  • One concept per test. Multiple assertions are fine when they verify the same behavior. Separate tests when behaviors are independent.
  • Fast by default. Unit tests should run in milliseconds. Gate slow tests behind groups:
    #[Group('slow')]
    .
  • Isolation is mandatory. Tests must not depend on execution order or shared mutable state. Each test sets up its own world.
  • 每个测试验证一个概念。当多个断言验证同一行为时是合理的。当行为独立时,拆分到不同测试中。
  • 默认保持快速。单元测试应在毫秒级运行。将慢测试标记为组:
    #[Group('slow')]
  • 必须保证隔离性。测试不能依赖执行顺序或共享可变状态。每个测试都应搭建独立的测试环境。

Fixtures

测试夹具

setUp() / tearDown()

setUp() / tearDown()

  • setUp()
    runs before each test method on a fresh instance. Create the SUT and its stubs here.
  • tearDown()
    runs after each test. Only needed for external resources (files, sockets, DB connections). Not needed for plain object cleanup.
  • setUpBeforeClass()
    /
    tearDownAfterClass()
    run once per class. Use for expensive shared resources (DB connections). Store in
    static
    properties.
php
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);
    }
}
  • setUp()
    在每个测试方法执行前运行,基于新实例创建被测系统(SUT)及其桩件。
  • tearDown()
    在每个测试方法执行后运行。仅在处理外部资源(文件、套接字、数据库连接)时需要。普通对象清理不需要此方法。
  • setUpBeforeClass()
    /
    tearDownAfterClass()
    在类级别仅运行一次。用于处理昂贵的共享资源(数据库连接)。存储在
    static
    属性中。
php
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);
    }
}

Fixture Lifecycle

夹具生命周期

  • setUpBeforeClass()
    — Class scope; once before first test
  • setUp()
    — Method scope; before each test
  • assertPreConditions()
    — Method scope; after setUp, before test
  • assertPostConditions()
    — Method scope; after test, before tearDown
  • tearDown()
    — Method scope; after each test
  • tearDownAfterClass()
    — Class scope; once after last test
  • Call
    parent::setUp()
    when extending abstract test cases — otherwise parent fixture setup is silently skipped.
  • Use
    #[Before]
    /
    #[After]
    attributes
    when multiple setup methods are needed (avoids fragile
    parent::setUp()
    chains).
  • setUpBeforeClass()
    — 类作用域;在第一个测试前运行一次
  • setUp()
    — 方法作用域;在每个测试前运行
  • assertPreConditions()
    — 方法作用域;在setUp之后、测试前运行
  • assertPostConditions()
    — 方法作用域;在测试之后、tearDown前运行
  • tearDown()
    — 方法作用域;在每个测试后运行
  • tearDownAfterClass()
    — 类作用域;在最后一个测试后运行一次
  • 当继承抽象测试用例时,调用
    parent::setUp()
    — 否则父类的夹具设置会被静默跳过。
  • 当需要多个设置方法时,使用
    #[Before]
    /
    #[After]
    属性
    — 避免脆弱的
    parent::setUp()
    调用链。

Data Providers

数据提供器

Basic Usage

基本用法

php
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],
    ];
}
  • Providers must be
    public static
    .
    Non-static providers are removed in PHPUnit 11.
  • Always use named datasets — string keys produce readable failure output.
  • Use
    #[DataProvider]
    attribute
    , not
    @dataProvider
    annotation.
php
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
    注解。

Inline Data

内联数据

For small, simple datasets — no provider method needed:
php
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);
}
适用于小型、简单的数据集——无需提供器方法:
php
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);
}

Generator Providers

生成器提供器

For large or computed datasets:
php
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];
}
适用于大型或计算型数据集:
php
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];
}

Provider Rules

提供器规则

  • Data must be scalar or immutable — no service objects or complex graphs in providers.
  • No mock objects in providers — framework isn't initialized during provider execution.
  • Empty providers are forbidden in PHPUnit 11 — throws
    InvalidDataProviderException
    .
  • Multiple providers can be stacked on one test method — datasets are combined.
See
${CLAUDE_SKILL_DIR}/references/data-providers.md
for external providers, TestDox integration, and edge cases.
  • 数据必须是标量或不可变的 — 提供器中不能包含服务对象或复杂对象图。
  • 提供器中不能包含模拟对象 — 框架在提供器执行期间未初始化。
  • PHPUnit 11中禁止空提供器 — 会抛出
    InvalidDataProviderException
  • 一个测试方法可以叠加多个提供器 — 数据集会被合并。
有关外部提供器、TestDox集成和边缘情况,请参阅
${CLAUDE_SKILL_DIR}/references/data-providers.md

Assertions

断言

Core Assertions

核心断言

php
$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
  • Prefer
    assertSame()
    over
    assertEquals()
    — strict type comparison catches more bugs.
  • Multiple assertions per test are fine when they verify the same behavior.
php
$this->assertSame($expected, $actual);        // 严格比较 ===(推荐)
$this->assertEquals($expected, $actual);       // 宽松比较 ==(谨慎使用)
$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);     // 严格比较
  • 优先使用
    assertSame()
    而非
    assertEquals()
    — 严格类型比较能捕获更多bug。
  • 每个测试可包含多个断言,只要它们验证同一行为。

String Assertions

字符串断言

php
$this->assertStringStartsWith('Error:', $message);
$this->assertStringEndsWith('.php', $filename);
$this->assertStringContainsString('needle', $haystack);
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}$/', $date);
php
$this->assertStringStartsWith('Error:', $message);
$this->assertStringEndsWith('.php', $filename);
$this->assertStringContainsString('needle', $haystack);
$this->assertMatchesRegularExpression('/^\d{4}-\d{2}$/', $date);

Float Comparison

浮点数比较

php
$this->assertEqualsWithDelta(3.14, $result, 0.01);
php
$this->assertEqualsWithDelta(3.14, $result, 0.01);

Exception Testing

异常测试

php
public function testThrowsOnInvalidInput(): void
{
    $this->expectException(InvalidArgumentException::class);
    $this->expectExceptionMessage('must be positive');

    $calculator->divide(1, 0);
}
  • Call
    expectException()
    before the throwing code
    — it sets up the expectation.
  • Use
    expectExceptionMessage()
    when the exception type is broad — validates the message contains the substring.
  • Use
    expectExceptionMessageMatches()
    for regex matching.
php
public function testThrowsOnInvalidInput(): void
{
    $this->expectException(InvalidArgumentException::class);
    $this->expectExceptionMessage('must be positive');

    $calculator->divide(1, 0);
}
  • 在抛出代码之前调用
    expectException()
    — 它会设置预期条件。
  • 当异常类型较宽泛时,使用
    expectExceptionMessage()
    — 验证消息是否包含指定子字符串。
  • 使用
    expectExceptionMessageMatches()
    进行正则匹配。

Deprecation / Error Expectations

弃用/错误预期

php
public function testTriggersDeprecation(): void
{
    $this->expectUserDeprecationMessage('use newMethod() instead');

    $service->oldMethod();
}
See
${CLAUDE_SKILL_DIR}/references/assertions.md
for the full assertion catalog, constraint system, and format string assertions.
php
public function testTriggersDeprecation(): void
{
    $this->expectUserDeprecationMessage('use newMethod() instead');

    $service->oldMethod();
}
有关完整的断言目录、约束系统和格式字符串断言,请参阅
${CLAUDE_SKILL_DIR}/references/assertions.md

Mocking

模拟

Stubs vs Mocks

桩件(Stub)vs 模拟对象(Mock)

  • Stub (
    createStub()
    ) — controls return values. No call verification.
  • Mock (
    createMock()
    ) — verifies interactions (method called, arguments matched).
Use stubs by default. Use mocks only when verifying that a side effect occurred.
  • 桩件
    createStub()
    )—— 控制返回值。不验证调用。
  • 模拟对象
    createMock()
    )—— 验证交互(方法是否被调用、参数是否匹配)。
默认使用桩件。仅当需要验证副作用发生时才使用模拟对象。

Creating Stubs

创建桩件

php
$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);
Shorthand for multiple methods:
php
$repo = $this->createConfiguredStub(UserRepository::class, [
    'find'   => new User(name: 'Alice'),
    'exists' => true,
]);
php
$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);
多个方法的简写形式:
php
$repo = $this->createConfiguredStub(UserRepository::class, [
    'find'   => new User(name: 'Alice'),
    'exists' => true,
]);

Creating Mocks

创建模拟对象

php
$logger = $this->createMock(Logger::class);
$logger->expects($this->once())
    ->method('error')
    ->with($this->stringContains('payment failed'));

$service = new PaymentService($logger);
$service->process($invalidPayment);
php
$logger = $this->createMock(Logger::class);
$logger->expects($this->once())
    ->method('error')
    ->with($this->stringContains('payment failed'));

$service = new PaymentService($logger);
$service->process($invalidPayment);

Return Value Configuration

返回值配置

php
$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'],
]);
php
$stub->method('fetch')->willReturn('value');              // 固定值
$stub->method('fetch')->willReturn('a', 'b', 'c');       // 连续值
$stub->method('fetch')->willReturnArgument(0);            // 返回第一个参数
$stub->method('fetch')->willReturnSelf();                 // 流畅接口
$stub->method('fetch')->willReturnCallback(fn ($id) => "item-{$id}");
$stub->method('fetch')->willThrowException(new RuntimeException('fail'));
$stub->method('fetch')->willReturnMap([
    ['key1', 'value1'],
    ['key2', 'value2'],
]);

Mocking Rules

模拟规则

  • Mock at boundaries. Mock external services, databases, filesystems, clocks — not internal functions.
  • Don't mock what you own when a fake or in-memory implementation is available.
  • Prefer dependency injection over complex mock setup. Pass collaborators as constructor parameters, stub in tests.
  • Never mock the thing you're testing. If you need to mock part of the SUT, the SUT has too many responsibilities — split it.
  • Favour interfaces over classes for test doubles — fewer limitations, better design.
  • Do not call
    expects()
    on stubs
    — deprecated in 11, error in 12.
See
${CLAUDE_SKILL_DIR}/references/mocking.md
for MockBuilder, intersection types, invocation matchers, and PHP 8.4 property hooks.
  • 在边界处进行模拟。模拟外部服务、数据库、文件系统、时钟——而非内部函数。
  • 当有假实现或内存实现可用时,不要模拟自己的代码
  • 优先使用依赖注入而非复杂的模拟设置。将协作者作为构造函数参数传递,在测试中使用桩件。
  • 永远不要模拟被测对象。如果需要模拟被测系统的一部分,说明被测系统职责过多——应拆分它。
  • 优先针对接口而非类创建测试替身——限制更少,设计更优。
  • 不要在桩件上调用
    expects()
    — 在PHPUnit 11中已被弃用,在12中会报错。
有关MockBuilder、交叉类型、调用匹配器和PHP 8.4属性钩子,请参阅
${CLAUDE_SKILL_DIR}/references/mocking.md

Attributes

属性

PHPUnit 11 uses PHP 8 attributes exclusively. All attributes are in the
PHPUnit\Framework\Attributes
namespace.
PHPUnit 11仅使用PHP 8属性。所有属性都位于
PHPUnit\Framework\Attributes
命名空间下。

Test Metadata

测试元数据

  • #[Test]
    — Mark non-
    test*
    method as a test
  • #[DataProvider('method')]
    — Connect a data provider
  • #[DataProviderExternal(Class::class, 'method')]
    — External data provider
  • #[TestWith([args])]
    — Inline data provider
  • #[TestDox('description')]
    — Custom TestDox description
  • #[Depends('testMethod')]
    — Declare test dependency
  • #[Group('name')]
    — Assign to group
  • #[Ticket('PROJ-123')]
    — Link to issue tracker
  • #[Test]
    — 将非
    test*
    方法标记为测试方法
  • #[DataProvider('method')]
    — 关联数据提供器
  • #[DataProviderExternal(Class::class, 'method')]
    — 外部数据提供器
  • #[TestWith([args])]
    — 内联数据提供器
  • #[TestDox('description')]
    — 自定义TestDox描述
  • #[Depends('testMethod')]
    — 声明测试依赖
  • #[Group('name')]
    — 分配到测试组
  • #[Ticket('PROJ-123')]
    — 关联到问题追踪系统

Skip / Conditional

跳过/条件执行

  • #[RequiresPhp('>= 8.4')]
    — Skip if PHP version doesn't match
  • #[RequiresPhpExtension('pdo_pgsql')]
    — Skip if extension missing
  • #[RequiresOperatingSystemFamily('Linux')]
    — Skip on other OS
  • #[RequiresFunction('sodium_crypto_sign')]
    — Skip if function missing
  • #[RequiresMethod(PDO::class, 'sqliteCreateFunction')]
    — Skip if method missing
  • #[RequiresPhp('>= 8.4')]
    — 如果PHP版本不匹配则跳过
  • #[RequiresPhpExtension('pdo_pgsql')]
    — 如果扩展缺失则跳过
  • #[RequiresOperatingSystemFamily('Linux')]
    — 在其他操作系统上跳过
  • #[RequiresFunction('sodium_crypto_sign')]
    — 如果函数缺失则跳过
  • #[RequiresMethod(PDO::class, 'sqliteCreateFunction')]
    — 如果方法缺失则跳过

Coverage

覆盖率

  • #[CoversClass(ClassName::class)]
    — Test covers this class
  • #[CoversFunction('functionName')]
    — Test covers this function
  • #[CoversMethod(ClassName::class, 'method')]
    — Test covers this method
  • #[CoversNothing]
    — Test contributes no coverage (integration tests)
  • #[UsesClass(ClassName::class)]
    — Allowed but not covered dependency
  • #[UsesFunction('functionName')]
    — Allowed but not covered function
  • #[CoversClass(ClassName::class)]
    — 测试覆盖此类
  • #[CoversFunction('functionName')]
    — 测试覆盖此函数
  • #[CoversMethod(ClassName::class, 'method')]
    — 测试覆盖此方法
  • #[CoversNothing]
    — 测试不贡献覆盖率(集成测试)
  • #[UsesClass(ClassName::class)]
    — 允许但不覆盖的依赖
  • #[UsesFunction('functionName')]
    — 允许但不覆盖的函数

Fixture

夹具

  • #[Before]
    — Run method before each test (alternative to setUp)
  • #[After]
    — Run method after each test (alternative to tearDown)
  • #[BeforeClass]
    — Run static method before first test
  • #[AfterClass]
    — Run static method after last test
  • #[BackupGlobals(true)]
    — Backup/restore globals for this test
  • #[BackupStaticProperties(true)]
    — Backup/restore static properties
  • #[Before]
    — 在每个测试前运行方法(替代setUp)
  • #[After]
    — 在每个测试后运行方法(替代tearDown)
  • #[BeforeClass]
    — 在第一个测试前运行静态方法
  • #[AfterClass]
    — 在最后一个测试后运行静态方法
  • #[BackupGlobals(true)]
    — 为此测试备份/恢复全局变量
  • #[BackupStaticProperties(true)]
    — 为此测试备份/恢复静态属性

Test Behavior

测试行为

  • #[DoesNotPerformAssertions]
    — Suppress risky test warning
  • #[RunInSeparateProcess]
    — Isolate in separate PHP process
  • #[RunTestsInSeparateProcesses]
    — All tests in class run isolated
  • #[Small]
    /
    #[Medium]
    /
    #[Large]
    — Time limit enforcement (1s/10s/60s)
  • #[DoesNotPerformAssertions]
    — 抑制风险测试警告
  • #[RunInSeparateProcess]
    — 在独立PHP进程中隔离执行
  • #[RunTestsInSeparateProcesses]
    — 类中所有测试均隔离执行
  • #[Small]
    /
    #[Medium]
    /
    #[Large]
    — 时间限制强制执行(1秒/10秒/60秒)

Test Organization

测试组织

Directory Structure

目录结构

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
  • Mirror source directory structure under
    tests/Unit/
    and
    tests/Integration/
    .
  • Unit tests — no database, no filesystem, no network. Mock all boundaries.
  • Integration tests — real dependencies. Mark with
    #[CoversNothing]
    to avoid polluting coverage metrics.
tests/
├── Unit/                     # 快速、隔离、无I/O操作
│   ├── Service/
│   │   └── PaymentServiceTest.php
│   └── Model/
│       └── UserTest.php
├── Integration/              # 使用真实依赖,速度较慢
│   └── Repository/
│       └── UserRepositoryTest.php
└── bootstrap.php             # 测试自动加载器
  • tests/Unit/
    tests/Integration/
    下镜像源码目录结构
  • 单元测试 — 无数据库、无文件系统、无网络。模拟所有边界。
  • 集成测试 — 使用真实依赖。标记为
    #[CoversNothing]
    以避免污染覆盖率指标。

Test Suites

测试套件

Define in
phpunit.xml
for selective execution:
xml
<testsuites>
    <testsuite name="unit">
        <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="integration">
        <directory>tests/Integration</directory>
    </testsuite>
</testsuites>
Run subsets:
phpunit --testsuite unit
,
phpunit --group slow
.
phpunit.xml
中定义以支持选择性执行:
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

Configuration

配置

Recommended phpunit.xml

推荐的phpunit.xml

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>
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>

Key Configuration Choices

关键配置选项

  • executionOrder="depends,random"
    — randomize test order to catch hidden dependencies while respecting explicit
    #[Depends]
    .
  • beStrictAboutTestsThatDoNotTestAnything="true"
    — flag tests without assertions as risky.
  • failOnDeprecation="true"
    — catch deprecations from your code early.
  • <source>
    with
    restrictDeprecations
    — only surface issues from your code, not vendor dependencies.
  • cacheDirectory
    — add
    .phpunit.cache
    to
    .gitignore
    .
  • executionOrder="depends,random"
    — 随机化测试顺序以发现隐藏依赖,同时尊重显式的
    #[Depends]
    声明。
  • beStrictAboutTestsThatDoNotTestAnything="true"
    — 将无断言的测试标记为风险测试。
  • failOnDeprecation="true"
    — 尽早捕获代码中的弃用警告。
  • restrictDeprecations
    <source>
    — 仅显示自身代码的问题,而非依赖库的问题。
  • cacheDirectory
    — 将
    .phpunit.cache
    添加到
    .gitignore

Code Coverage

代码覆盖率

Requires PCOV or Xdebug extension:
bash
phpunit --coverage-html build/coverage --coverage-clover build/clover.xml
Use
#[CoversClass]
and
#[UsesClass]
attributes to target coverage precisely. With
beStrictAboutCoverageMetadata="true"
, tests without coverage attributes are risky.
See
${CLAUDE_SKILL_DIR}/references/configuration.md
for the full XML reference, coverage report types, and execution order options.
需要PCOV或Xdebug扩展:
bash
phpunit --coverage-html build/coverage --coverage-clover build/clover.xml
使用
#[CoversClass]
#[UsesClass]
属性精准定位覆盖率。当设置
beStrictAboutCoverageMetadata="true"
时,无覆盖率属性的测试会被标记为风险测试。
有关完整的XML参考、覆盖率报告类型和执行顺序选项,请参阅
${CLAUDE_SKILL_DIR}/references/configuration.md

Application

应用场景

When writing tests: apply all conventions silently — don't narrate each rule being followed. Match the project's existing test style. If an existing codebase contradicts a convention, follow the codebase and flag the divergence once.
When reviewing tests: cite the specific issue and show the fix inline. Don't lecture — state what's wrong and how to fix it.
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)"
编写测试时:静默应用所有约定——无需说明每条规则。匹配项目现有的测试风格。如果现有代码库与约定冲突,请遵循代码库并一次性标记差异。
评审测试时:指出具体问题并展示内联修复。不要说教——说明问题所在及修复方法。
错误示例: "根据PHPUnit最佳实践,当不需要预期时,应使用createStub
       而非createMock..."
正确示例: "createMock → createStub(未调用expects(),桩件已足够)"

Integration

集成

The php skill governs language choices; this skill governs PHPUnit testing decisions. The coding skill governs workflow (discovery, planning, verification).
Test behavior, not implementation. When in doubt, mock less.
php技能管理语言选择;本技能管理PHPUnit测试决策。coding技能管理工作流(发现、规划、验证)。
测试行为,而非实现。如有疑问,减少模拟。