Claude
Skills
Sign in
Back

phpunit

Included with Lifetime
$97 forever

PHPUnit testing framework conventions and practices. Invoke whenever task involves any interaction with PHPUnit — writing tests, configuring PHPUnit, data providers, mocking, assertions, debugging test failures, or coverage.

Productivity

What this skill does


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

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

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

### Arrange-Act-Assert

Structure every test in three phases:

```php
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());
}
```

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

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

## Fixtures

### 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);
    }
}
```

### 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).

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

### 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);
}
```

### 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];
}
```

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

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

### String Assertions

```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);
```

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

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

## Mocking

### Stubs vs Mocks

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

Related in Productivity