phpunit skill
✓ VerifiedWrite, configure, and debug PHP unit tests using assertions, mocks, stubs, and data providers.
- GitHub stars
- 23
- Files
- 6
- Last commit
- Nov 11, 2026
- License
- MIT
- Allowed tools
- —
What does the phpunit skill do?
phpunit is an agent skill. Write, configure, and debug PHP unit tests using assertions, mocks, stubs, and data providers.
Summary generated from the skill's SKILL.md.
About the phpunit skill
Developers working with PHP 8.5 and PHPUnit 11+ can use this skill to automate the creation and maintenance of unit and integration tests. It provides the necessary patterns for writing executable documentation through test case classes that extend TestCase. The skill focuses on the Arrange-Act-Assert pattern to ensure tests are clear and verify behavior rather than implementation details.
When an agent is tasked with testing PHP code, it uses this skill to implement assertions, configure test suites via phpunit.xml, and manage test lifecycles using setUp and tearDown methods. It supports advanced testing techniques such as creating stubs with createStub for controlling return values and using createMock for verifying method interactions. The skill relies on PHP 8 attributes like #[Test], #[DataProvider], and #[Group] instead of deprecated annotations to manage test metadata and conditional execution. It also assists in debugging failures by providing guidance on exception expectations and coverage reporting.
What an agent can do with it
- 1Generate unit tests for a serviceCreate a final test class that mirrors the source structure and uses the Arrange-Act-Assert pattern to verify a specific class.
- 2Verify error handling and exceptionsWrite tests that ensure specific exceptions are thrown when invalid input is provided to a method.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the phpunit skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the phpunit skill in Plugin
/plugin marketplace add xobotyi/cc-foundry /plugin install php@foundry
Install the phpunit skill in Claude Code
~/.claude/skills/phpunit/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit ~/.claude/skills/phpunitInstall the phpunit skill in Claude Code (project)
.claude/skills/phpunit/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit .claude/skills/phpunitInstall the phpunit skill in Codex
~/.agents/skills/phpunit/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit ~/.agents/skills/phpunitInstall the phpunit skill in Gemini CLI
~/.gemini/skills/phpunit/SKILL.md
$ npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit ~/.gemini/skills/phpunitInstall the phpunit skill in Claude.ai
npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit phpunit && zip -r phpunit.zip phpunit # claude.ai → Customize → Skills → + → Create skill → Upload a skill → phpunit.zip
Automated checks
Ran Nov 14, 2026Full report →Passed our automated checks. That is not a security audit: read the scripts before you let an agent run them. How checks work
- Source repository linked and reachablexobotyi/cc-foundry/plugins/php/skills/phpunit is public and reachablegithub.com/xobotyi/cc-foundry/tree/HEAD/plugins/php/skills/p ↗Pass
- Open source license (OSI)MITspdx.org/licenses/MIT.html ↗Pass
- Commit or release in the last yearLast commit 2026-11-11github.com/xobotyi/cc-foundry/commits ↗Pass
- No download-and-run or credential theft in scriptsNo scripts in the skill folderPass
- No hidden instructions in SKILL.mdNo override phrases, safeguard switches, credential requests or hidden charactersgithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/php/skills/p ↗Pass
- Pre-approved tools are scopedNo pre-approved tools; the agent asks before using onegithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/php/skills/p ↗Pass
- SKILL.md follows the Agent Skills formatValid name and description; says when to use it; 472 linesgithub.com/xobotyi/cc-foundry/blob/HEAD/plugins/php/skills/p ↗Pass
SKILL.md
View on GitHubPHPUnit
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 testsetUp()— Method scope; before each testassertPreConditions()— Method scope; after setUp, before testassertPostConditions()— Method scope; after test, before tearDowntearDown()— Method scope; after each testtearDownAfterClass()— 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.
Files in this skill
6phpunit skill questions
How do I install the phpunit skill in Claude Code?
Run npx degit xobotyi/cc-foundry/plugins/php/skills/phpunit ~/.claude/skills/phpunit. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the phpunit skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
Are annotations still supported in PHPUnit 11?
No, PHPUnit 11 uses PHP 8 attributes exclusively; annotations are deprecated in version 11 and will be removed in version 12.