Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
xobotyi avatar

Phpunit

  • 295 installs
  • 19 repo stars
  • Updated August 1, 2026
  • xobotyi/cc-foundry

Helps with ai & agent building tasks.

About

phpunit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.

  • phpunit
  • AI & Agent Building
  • AI-coding skill

Phpunit by the numbers

  • 295 all-time installs (skills.sh)
  • +4 installs in the week ending Jul 27, 2026 (Skillselion tracking)
  • Ranked #2,319 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill phpunit

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs295
repo stars19
Last updatedAugust 1, 2026
Repositoryxobotyi/cc-foundry

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

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:

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.

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.

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

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:

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:

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

$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

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

Float Comparison

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

Exception Testing

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

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 effect occurred.

Creating Stubs

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

$repo = $this->createConfiguredStub(UserRepository::class, [
    'find'   => new User(name: 'Alice'),
    'exists' => true,
]);

Creating Mocks

$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

$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'],
]);

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.

Attributes

PHPUnit 11 uses PHP 8 attributes exclusively. All attributes are in the PHPUnit\Framework\Attributes namespace.

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

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

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

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

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)

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.

Test Suites

Define in phpunit.xml for selective execution:

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

Configuration

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

Code Coverage

Requires PCOV or Xdebug extension:

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.

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

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.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.