Loading...
Loading...
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 skill4agent add xobotyi/cc-foundry phpunitTestCasesetUp()tearDown()${CLAUDE_SKILL_DIR}/references/assertions.md${CLAUDE_SKILL_DIR}/references/mocking.md${CLAUDE_SKILL_DIR}/references/data-providers.md${CLAUDE_SKILL_DIR}/references/configuration.md*Test.phpsrc/Service/PaymentService.phptests/Unit/Service/PaymentServiceTest.phpfinal class PaymentServiceTest extends TestCasefinaltest#[Test]testReturnsEmptyCollectionWhenNoResultstestSearchpublic 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()tearDown()setUpBeforeClass()tearDownAfterClass()staticfinal 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()setUp()assertPreConditions()assertPostConditions()tearDown()tearDownAfterClass()parent::setUp()#[Before]#[After]parent::setUp()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#[DataProvider]@dataProvideruse 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);
}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${CLAUDE_SKILL_DIR}/references/data-providers.md$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 comparisonassertSame()assertEquals()$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()expectExceptionMessage()expectExceptionMessageMatches()public function testTriggersDeprecation(): void
{
$this->expectUserDeprecationMessage('use newMethod() instead');
$service->oldMethod();
}${CLAUDE_SKILL_DIR}/references/assertions.mdcreateStub()createMock()$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);$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()${CLAUDE_SKILL_DIR}/references/mocking.mdPHPUnit\Framework\Attributes#[Test]test*#[DataProvider('method')]#[DataProviderExternal(Class::class, 'method')]#[TestWith([args])]#[TestDox('description')]#[Depends('testMethod')]#[Group('name')]#[Ticket('PROJ-123')]#[RequiresPhp('>= 8.4')]#[RequiresPhpExtension('pdo_pgsql')]#[RequiresOperatingSystemFamily('Linux')]#[RequiresFunction('sodium_crypto_sign')]#[RequiresMethod(PDO::class, 'sqliteCreateFunction')]#[CoversClass(ClassName::class)]#[CoversFunction('functionName')]#[CoversMethod(ClassName::class, 'method')]#[CoversNothing]#[UsesClass(ClassName::class)]#[UsesFunction('functionName')]#[Before]#[After]#[BeforeClass]#[AfterClass]#[BackupGlobals(true)]#[BackupStaticProperties(true)]#[DoesNotPerformAssertions]#[RunInSeparateProcess]#[RunTestsInSeparateProcesses]#[Small]#[Medium]#[Large]tests/
├── Unit/ # Fast, isolated, no I/O
│ ├── Service/
│ │ └── PaymentServiceTest.php
│ └── Model/
│ └── UserTest.php
├── Integration/ # Real dependencies, slower
│ └── Repository/
│ └── UserRepositoryTest.php
└── bootstrap.php # Autoloader for teststests/Unit/tests/Integration/#[CoversNothing]phpunit.xml<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>phpunit --testsuite unitphpunit --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"#[Depends]beStrictAboutTestsThatDoNotTestAnything="true"failOnDeprecation="true"<source>restrictDeprecationscacheDirectory.phpunit.cache.gitignorephpunit --coverage-html build/coverage --coverage-clover build/clover.xml#[CoversClass]#[UsesClass]beStrictAboutCoverageMetadata="true"${CLAUDE_SKILL_DIR}/references/configuration.mdBad: "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)"