From php
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.
npx claudepluginhub xobotyi/cc-foundry --plugin phpThis skill uses the workspace's default tool permissions.
**Test behavior, not implementation. Tests are executable documentation — if the test name doesn't explain what the code
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
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 rich assertion API. All patterns target PHPUnit 11+ on PHP 8.5+. Use PHP 8 attributes exclusively —
annotations are deprecated in 11, removed in 12.
${CLAUDE_SKILL_DIR}/references/assertions.md] — Full
assertion API grouped by category, constraint system, custom assertions${CLAUDE_SKILL_DIR}/references/mocking.md] — createStub vs
createMock, return config, invocation matchers, argument constraints, MockBuilder${CLAUDE_SKILL_DIR}/references/data-providers.md]
— #[DataProvider], #[TestWith], named datasets, generator providers, external providers${CLAUDE_SKILL_DIR}/references/configuration.md] — XML
elements, strict settings, source element, coverage reports, execution order*Test.php in configured test directories. Mirror source structure: src/Service/PaymentService.php →
tests/Unit/Service/PaymentServiceTest.php.final class PaymentServiceTest extends TestCase. Always final.test prefix or #[Test] attribute. Describe the behavior: testReturnsEmptyCollectionWhenNoResults
not testSearch.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());
}
#[Group('slow')].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);
}
}
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).
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. Non-static providers are removed in PHPUnit 11.#[DataProvider] attribute, not @dataProvider annotation.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);
}
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];
}
InvalidDataProviderException.See ${CLAUDE_SKILL_DIR}/references/data-providers.md for external providers, TestDox integration, and edge cases.
$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() over assertEquals() — strict type comparison catches more bugs.$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() before the throwing code — it sets up the expectation.expectExceptionMessage() when the exception type is broad — validates the message contains the substring.expectExceptionMessageMatches() for regex matching.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.
createStub()) — controls return values. No call verification.createMock()) — verifies interactions (method called, arguments matched).Use stubs by default. Use mocks only when verifying that a side effect occurred.
$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,
]);
$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'],
]);
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.
PHPUnit 11 uses PHP 8 attributes exclusively. All attributes are in the PHPUnit\Framework\Attributes namespace.
#[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#[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#[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#[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#[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)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/ and tests/Integration/.#[CoversNothing] to avoid polluting coverage metrics.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.
<?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" — 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.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.
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)"
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.