phpunit
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 do I install this agent skill?
npx skills add https://github.com/xobotyi/cc-foundry --skill phpunitIs this agent skill safe to install?
- Gen Agent Trust Hubpass
The skill provides comprehensive and safe guidelines for using the PHPUnit 11+ testing framework, following official documentation and best practices.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
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.phpin configured test directories. Mirror source structure:src/Service/PaymentService.php→tests/Unit/Service/PaymentServiceTest.php. - Classes:
final class PaymentServiceTest extends TestCase. Alwaysfinal. - Methods:
testprefix or#[Test]attribute. Describe the behavior:testReturnsEmptyCollectionWhenNoResultsnottestSearch. - 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 instaticproperties.
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 fragileparent::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@dataProviderannotation.
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()overassertEquals()— 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/andtests/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>withrestrictDeprecations— only surface issues from your code, not vendor dependencies.cacheDirectory— add.phpunit.cacheto.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.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/xobotyi/cc-foundry/phpunit">View phpunit on skillZs</a>