From php-foundation
PHPUnit and Pest patterns for any PHP project: test structure, naming, data providers, test doubles (mocks/stubs/fakes), fixtures, and coverage targets. Stack-agnostic — referenced by every PHP plugin in the marketplace. Use this skill to: - Structure unit and integration tests with PHPUnit or Pest consistently. - Drive cases with data providers / datasets instead of copy-pasted test bodies. - Use test doubles with discipline — mock collaborators, not the system under test. - Set up and tear down fixtures cleanly and aim for a meaningful coverage target. Do NOT use this skill for: - PHP language idioms — see php-foundation:php-conventions. - Composer / autoloading — see php-foundation:composer-tooling. - Framework-specific test helpers (Laravel RefreshDatabase/actingAs, Symfony WebTestCase/KernelTestCase) — those are injected by the framework plugin's QA phase.
How this skill is triggered — by the user, by Claude, or both
Slash command
/php-foundation:php-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Patterns for PHPUnit and Pest that apply to any PHP codebase. Detect which runner the project uses before writing tests: `pestphp/pest` in `require-dev` → Pest; otherwise PHPUnit. Both run on top of PHPUnit assertions.
Patterns for PHPUnit and Pest that apply to any PHP codebase. Detect which runner the project uses before writing tests: pestphp/pest in require-dev → Pest; otherwise PHPUnit. Both run on top of PHPUnit assertions.
grep -q pestphp/pest composer.json && echo pest || echo phpunit
vendor/bin/phpunit # PHPUnit
vendor/bin/pest # Pest
vendor/bin/phpunit --coverage-text # coverage (needs Xdebug or PCOV)
Prefer a composer test script if one exists.
PHPUnit (class-based):
<?php
declare(strict_types=1);
namespace App\Tests\Unit;
use App\Domain\Money;
use App\Domain\Currency;
use PHPUnit\Framework\TestCase;
final class MoneyTest extends TestCase
{
public function test_it_adds_two_amounts_of_the_same_currency(): void
{
// Arrange
$a = new Money(100, Currency::USD);
$b = new Money(250, Currency::USD);
// Act
$sum = $a->add($b);
// Assert
self::assertSame(350, $sum->amount);
}
public function test_it_rejects_a_currency_mismatch(): void
{
$this->expectException(\InvalidArgumentException::class);
(new Money(100, Currency::USD))->add(new Money(100, Currency::EUR));
}
}
Pest (function-based, same engine):
<?php
declare(strict_types=1);
use App\Domain\Money;
use App\Domain\Currency;
it('adds two amounts of the same currency', function () {
$sum = (new Money(100, Currency::USD))->add(new Money(250, Currency::USD));
expect($sum->amount)->toBe(350);
});
it('rejects a currency mismatch', function () {
expect(fn () => (new Money(100, Currency::USD))->add(new Money(100, Currency::EUR)))
->toThrow(InvalidArgumentException::class);
});
tests/, namespace App\Tests\... (mapped via autoload-dev).src/Domain/Money.php → tests/Unit/Domain/MoneyTest.php.tests/Unit (pure, no I/O) from tests/Integration / tests/Feature (DB, HTTP, filesystem).test_it_<behaviour>() or #[Test] attribute. Pest: it('<behaviour>'). Describe behaviour, not implementation.Drive many cases through one body instead of duplicating tests.
// PHPUnit
public static function discountCases(): array
{
return [
'no discount below 100' => [50, 0.0],
'5% at 100' => [100, 0.05],
'15% at 1000' => [1000, 0.15],
];
}
#[DataProvider('discountCases')]
public function test_discount_tiers(int $total, float $expected): void
{
self::assertSame($expected, discountFor($total));
}
// Pest
it('applies discount tiers', function (int $total, float $expected) {
expect(discountFor($total))->toBe($expected);
})->with([
[50, 0.0],
[100, 0.05],
[1000, 0.15],
]);
public function test_it_sends_a_receipt_after_charging(): void
{
$gateway = $this->createMock(PaymentGateway::class);
$gateway->expects(self::once())
->method('charge')
->with(self::equalTo(500))
->willReturn(new Receipt('rcpt_1'));
$mailer = $this->createMock(Mailer::class);
$mailer->expects(self::once())->method('send');
$service = new CheckoutService($gateway, $mailer);
$service->checkout($order);
}
Discipline:
protected function setUp(): void
{
parent::setUp();
$this->repository = new InMemoryUserRepository();
}
setUp() for common arrange steps; keep heavy fixtures out of unit tests.vendor/bin/phpunit --coverage-text (requires PCOV or Xdebug). Report the number; do not chase the last few percent on glue code.npx claudepluginhub aratkruglik/claude-sdlc --plugin php-foundationPHPUnit 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.
Writes PHPUnit tests for PHP code: unit tests, mocking, data providers, test doubles, assertions, and TDD practices. Use for testing PHP apps including Magento.
Provides PHP 8.4 testing knowledge: pyramid, AAA pattern, naming conventions, isolation principles, DDD guidelines, PHPUnit/Pest patterns.