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.
How this skill is triggered — by the user, by Claude, or both
Slash command
/php:phpunitThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Test behavior, not implementation. Tests are executable documentation — if the test name doesn't explain what the 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' => '[email protected]'];
// 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.
npx claudepluginhub xobotyi/cc-foundry --plugin phpWrites PHPUnit tests for PHP code: unit tests, mocking, data providers, test doubles, assertions, and TDD practices. Use for testing PHP apps including Magento.
Writes PHPUnit tests for PHP projects: unit tests, assertions, mocks, stubs, data providers, exception testing, TDD workflows. Supports WooCommerce via WC_Unit_Test_Case.
Generates isolated PHPUnit unit tests for PHP 8.4 classes using AAA pattern, descriptive naming, and attributes. Supports Value Objects, Entities, Services.