php-testing
PHP testing patterns: PHPUnit 11 with mocks and data providers, Pest v3 with expectations and datasets, Laravel feature/HTTP tests with RefreshDatabase, Symfony WebTestCase, PHPStan static analysis, Infection mutation testing. Use when writing or reviewing PHP tests.
Best use case
php-testing is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
PHP testing patterns: PHPUnit 11 with mocks and data providers, Pest v3 with expectations and datasets, Laravel feature/HTTP tests with RefreshDatabase, Symfony WebTestCase, PHPStan static analysis, Infection mutation testing. Use when writing or reviewing PHP tests.
Teams using php-testing should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/php-testing/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How php-testing Compares
| Feature / Agent | php-testing | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
PHP testing patterns: PHPUnit 11 with mocks and data providers, Pest v3 with expectations and datasets, Laravel feature/HTTP tests with RefreshDatabase, Symfony WebTestCase, PHPStan static analysis, Infection mutation testing. Use when writing or reviewing PHP tests.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# PHP Testing
## When to Activate
- Writing PHP tests with PHPUnit or Pest
- Setting up Laravel/Symfony test suites
- Configuring PHPStan for static analysis
- Adding mutation testing with Infection
- Applying TDD layer by layer: domain value objects first, then application handlers, then infrastructure repositories, then HTTP controllers
- Verifying that test assertions are meaningful (not just coverage-padding) by running Infection mutation testing with an 80% MSI gate
- Choosing between PHPUnit data providers and Pest datasets to parameterize tests across multiple input variants
---
## PHPUnit 11 — Unit Tests
```php
<?php
declare(strict_types=1);
namespace Tests\Unit;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use App\Domain\Email;
class EmailTest extends TestCase
{
#[Test]
public function it_normalizes_email_to_lowercase(): void
{
$email = new Email('Alice@EXAMPLE.COM');
$this->assertSame('alice@example.com', $email->value);
}
#[Test]
public function it_throws_on_invalid_email(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Invalid email');
new Email('not-an-email');
}
#[Test]
#[DataProvider('validEmails')]
public function it_accepts_valid_email_formats(string $input, string $expected): void
{
$this->assertSame($expected, (new Email($input))->value);
}
public static function validEmails(): array
{
return [
'simple' => ['alice@example.com', 'alice@example.com'],
'mixed case' => ['ALICE@EXAMPLE.COM', 'alice@example.com'],
'subdomain' => ['alice@mail.example.com', 'alice@mail.example.com'],
];
}
}
```
### Mocking with PHPUnit
```php
<?php
declare(strict_types=1);
class RegisterUserHandlerTest extends TestCase
{
private UserRepository $users;
private PasswordHasher $hasher;
private EventBus $events;
private RegisterUserHandler $handler;
protected function setUp(): void
{
$this->users = $this->createMock(UserRepository::class);
$this->hasher = $this->createMock(PasswordHasher::class);
$this->events = $this->createMock(EventBus::class);
$this->handler = new RegisterUserHandler($this->users, $this->hasher, $this->events);
}
#[Test]
public function it_registers_a_new_user(): void
{
$this->users->method('findByEmail')->willReturn(null);
$this->hasher->method('hash')->willReturn('hashed_pw');
$this->users->expects($this->once())->method('save');
$this->events->expects($this->once())->method('dispatch')
->with($this->isInstanceOf(UserRegistered::class));
$user = $this->handler->handle(new RegisterUserCommand(
name: 'Alice',
email: 'alice@example.com',
password: 'secure_password_123',
));
$this->assertSame('alice@example.com', (string) $user->getEmail());
}
#[Test]
public function it_throws_on_duplicate_email(): void
{
$this->users->method('findByEmail')->willReturn(new User());
$this->expectException(DuplicateEmailException::class);
$this->handler->handle(new RegisterUserCommand(
name: 'Bob',
email: 'existing@example.com',
password: 'password_123',
));
}
}
```
---
## Pest v3 — Expressive Syntax
```php
<?php
use App\Domain\Email;
use App\Handler\RegisterUserHandler;
// describe + it grouping
describe('Email', function () {
it('normalizes to lowercase', function () {
expect(new Email('ALICE@EXAMPLE.COM'))
->value->toBe('alice@example.com');
});
it('throws on invalid input', function () {
expect(fn () => new Email('bad'))->toThrow(\InvalidArgumentException::class);
});
});
// Dataset (Pest equivalent of DataProvider)
it('accepts valid email formats', function (string $input, string $expected) {
expect((new Email($input))->value)->toBe($expected);
})->with([
'simple' => ['alice@example.com', 'alice@example.com'],
'mixed case' => ['ALICE@EXAMPLE.COM', 'alice@example.com'],
]);
// Pest mock via mockery
it('dispatches event on registration', function () {
$users = mock(UserRepository::class)->allows('findByEmail')->andReturn(null)->allows('save');
$hasher = mock(PasswordHasher::class)->allows('hash')->andReturn('hash');
$events = mock(EventBus::class)->expects('dispatch')->once();
$handler = new RegisterUserHandler($users, $hasher, $events);
$handler->handle(new RegisterUserCommand('Alice', 'alice@example.com', 'pw'));
});
```
---
## Laravel Feature Tests
```php
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserApiTest extends TestCase
{
use RefreshDatabase;
public function test_create_user_returns_201(): void
{
$response = $this->postJson('/api/users', [
'name' => 'Alice',
'email' => 'alice@example.com',
'password' => 'super_secure_pass',
'password_confirmation' => 'super_secure_pass',
]);
$response->assertStatus(201)
->assertJsonPath('data.email', 'alice@example.com');
$this->assertDatabaseHas('users', ['email' => 'alice@example.com']);
}
public function test_create_user_validates_email(): void
{
$response = $this->postJson('/api/users', [
'name' => 'Bob',
'email' => 'not-an-email',
]);
$response->assertUnprocessable()
->assertJsonValidationErrors(['email', 'password']);
}
public function test_duplicate_email_returns_422(): void
{
User::factory()->create(['email' => 'alice@example.com']);
$this->postJson('/api/users', [
'name' => 'Alice 2',
'email' => 'alice@example.com',
'password' => 'super_secure_pass',
'password_confirmation' => 'super_secure_pass',
])->assertUnprocessable();
}
}
```
---
## Symfony WebTestCase
```php
<?php
declare(strict_types=1);
namespace Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class UserControllerTest extends WebTestCase
{
public function testRegisterUser(): void
{
$client = static::createClient();
$client->request('POST', '/users', [], [], [
'CONTENT_TYPE' => 'application/json',
], json_encode([
'name' => 'Alice',
'email' => 'alice@example.com',
'password' => 'super_secure_pass',
]));
$this->assertResponseStatusCodeSame(201);
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertSame('alice@example.com', $data['email']);
}
}
```
---
## PHPStan Static Analysis
```bash
vendor/bin/phpstan analyse src/ --level=9
```
`phpstan.neon`:
```neon
parameters:
level: 9
paths:
- src
checkMissingIterableValueType: true
checkGenericClassInNonGenericObjectType: true
```
---
## Infection — Mutation Testing
Mutation testing verifies that tests actually catch bugs:
```bash
vendor/bin/infection --min-msi=80 --min-covered-msi=85
```
`infection.json5`:
```json
{
"source": { "directories": ["src"] },
"minMsi": 80,
"minCoveredMsi": 85,
"testFramework": "phpunit"
}
```
High MSI (Mutation Score Indicator) confirms test assertions are meaningful, not just coverage-chasing.
---
## Strategy: Layer by Layer
| Layer | Framework | Focus |
|-------|-----------|-------|
| Domain (pure PHP) | PHPUnit / Pest | Value objects, domain logic |
| Application (handlers) | PHPUnit with mocks | Command/query handlers |
| Infrastructure (DB) | TestCase + real DB | Repository implementations |
| HTTP (controllers) | Laravel HTTP tests / Symfony WebTestCase | Endpoints, validation, responses |Related Skills
visual-testing
Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.
typescript-testing
TypeScript testing patterns: Vitest for unit/integration, Playwright for E2E, MSW for API mocking, Testing Library for React components. Core TDD methodology for TypeScript/JavaScript projects.
swift-testing
Swift testing patterns: Swift Testing framework (Swift 6+), XCTest for UI tests, async/await test cases, actor testing, Combine testing, and XCUITest for UI automation. TDD for Swift/SwiftUI.
swift-protocol-di-testing
Protocol-based dependency injection for testable Swift code — mock file system, network, and external APIs using focused protocols and Swift Testing.
scala-testing
Scala testing with ScalaTest, MUnit, and ScalaCheck: FunSpec/FlatSpec test structure, property-based testing with forAll, mocking with MockitoSugar, Cats Effect testing with munit-cats-effect (runTest/IOSuite), ZIO Test, Testcontainers-Scala for database integration tests, and CI integration with sbt. Use when writing or reviewing Scala tests.
rust-testing
Rust testing patterns — unit tests with mockall, integration tests with sqlx transactions, HTTP handler testing (axum), benchmarks (criterion), property tests (proptest), fuzzing, and CI with cargo-nextest.
rust-testing-advanced
Advanced Rust testing anti-patterns and corrections — cfg(test) placement, expect() over unwrap(), mockall expectation ordering, executor mixing (#[tokio::test] vs block_on), PgPool isolation with
ruby-testing
RSpec testing patterns for Ruby and Rails — factories, mocks, request specs, feature specs, VCR, and SimpleCov coverage.
r-testing
R testing patterns: testthat 3e with expect_* assertions, snapshot testing, mocking with mockery and httptest2, covr code coverage, lintr static analysis, property-based testing with hedgehog, testing Shiny apps with shinytest2. Use when writing or reviewing R tests.
python-testing
Python testing strategies using pytest, TDD methodology, fixtures, mocking, and parametrization. Core testing fundamentals.
python-testing-advanced
Advanced Python testing — async testing with pytest-asyncio, exception/side-effect testing, test organization, common patterns (API, database, class methods), pytest configuration, and CLI reference. Extends python-testing.
load-testing
Load and performance testing with k6 (TypeScript/Go/any HTTP) and Locust (Python). Covers test types (smoke, load, stress, spike, soak), SLO thresholds, CI integration, and interpreting results.