
Phpunit
- 295 installs
- 19 repo stars
- Updated August 1, 2026
- xobotyi/cc-foundry
Helps with ai & agent building tasks.
About
phpunit is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- phpunit
- AI & Agent Building
- AI-coding skill
Phpunit by the numbers
- 295 all-time installs (skills.sh)
- +4 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #2,319 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/xobotyi/cc-foundry --skill phpunitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 295 |
|---|---|
| repo stars | ★ 19 |
| Last updated | August 1, 2026 |
| Repository | xobotyi/cc-foundry ↗ |
What it does
Helps with ai & agent building tasks.
Files
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:testReturnsEmptyCollectionWhenNoResults
not testSearch.
- 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 in static properties.
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 fragile
parent::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()` over `assertEquals()` — 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>` with `restrictDeprecations` — 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.xmlUse #[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.
{
"sources": {
"Writing Tests": "https://docs.phpunit.de/en/11.5/writing-tests-for-phpunit.html",
"Assertions": "https://docs.phpunit.de/en/11.5/assertions.html",
"Test Doubles": "https://docs.phpunit.de/en/11.5/test-doubles.html",
"Fixtures": "https://docs.phpunit.de/en/11.5/fixtures.html",
"Organizing Tests": "https://docs.phpunit.de/en/11.5/organizing-tests.html",
"Configuration": "https://docs.phpunit.de/en/11.5/configuration.html",
"Code Coverage": "https://docs.phpunit.de/en/11.5/code-coverage.html",
"Attributes": "https://docs.phpunit.de/en/11.5/attributes.html",
"Error Handling": "https://docs.phpunit.de/en/11.5/error-handling.html",
"Risky Tests": "https://docs.phpunit.de/en/11.5/risky-tests.html",
"PHPUnit 11 Announcement": "https://phpunit.de/announcements/phpunit-11.html",
"PHPUnit 12 Announcement": "https://phpunit.de/announcements/phpunit-12.html"
},
"lastFetched": "2026-03-02T11:54:26.327Z"
}
Assertions Reference
PHPUnit assertions are declared static on PHPUnit\Framework\Assert, inherited by TestCase. Invoke as $this->assertX(), self::assertX(), or the global wrapper assertX().
Identity (Strict ===)
assertSame(mixed $expected, mixed $actual[, string $message])— type + value match; for objects, checks same
reference
assertNotSame(mixed $expected, mixed $actual[, string $message])
Equality (Loose ==)
assertEquals(mixed $expected, mixed $actual[, string $message])assertNotEquals(mixed $expected, mixed $actual[, string $message])assertEqualsCanonicalizing(...)— ignores element order in arraysassertEqualsIgnoringCase(...)— case-insensitive string comparisonassertEqualsWithDelta(float $expected, float $actual, float $delta[, string $message])— floating-point toleranceassertObjectEquals(object $expected, object $actual, string $method[, string $message])— calls
$actual->$method($expected), asserts it returns true
Boolean
assertTrue(bool $condition[, string $message])assertNotTrue(...)/assertFalse(...)/assertNotFalse(...)
Null
assertNull(mixed $value[, string $message])assertNotNull(mixed $value[, string $message])
Type
assertInstanceOf(string $expected, mixed $actual[, string $message])assertNotInstanceOf(string $expected, mixed $actual[, string $message])
Comparison
assertGreaterThan(mixed $expected, mixed $actual[, string $message])assertGreaterThanOrEqual(...)/assertLessThan(...)/assertLessThanOrEqual(...)
String
assertStringStartsWith(string $prefix, string $string[, string $message])assertStringEndsWith(string $suffix, string $string[, string $message])assertStringContainsString(string $needle, string $haystack[, string $message])assertStringContainsStringIgnoringCase(...)assertStringNotContainsString(...)/assertStringNotContainsStringIgnoringCase(...)assertMatchesRegularExpression(string $pattern, string $string[, string $message])assertDoesNotMatchRegularExpression(...)assertStringMatchesFormat(string $format, string $string[, string $message])— format placeholders:%e(dir sep),
%s (string), %S (optional string), %a (anything), %w (whitespace), %i (integer), %d (unsigned int), %x (hex), %f (float), %c (single char)
assertStringEqualsFile(string $expectedFile, string $actualString[, string $message])
Array / Iterable
assertArrayHasKey(int|string $key, array|ArrayAccess $array[, string $message])assertArrayNotHasKey(...)assertContains(mixed $needle, iterable $haystack[, string $message])— strict comparisonassertNotContains(...)assertContainsOnly(string $type, iterable $haystack[, ?bool $isNativeType, string $message])assertContainsOnlyInstancesOf(string $className, iterable $haystack[, string $message])assertCount(int $expectedCount, Countable|iterable $haystack[, string $message])assertNotCount(...)assertSameSize(Countable|iterable $expected, Countable|iterable $actual[, string $message])assertEmpty(mixed $actual[, string $message])/assertNotEmpty(...)assertArrayIsIdenticalToArrayOnlyConsideringListOfKeys(array $expected, array $actual, array $keysToBeConsidered[, string $message])
— compare arrays considering only specified keys
assertArrayIsIdenticalToArrayIgnoringListOfKeys(array $expected, array $actual, array $keysToBeIgnored[, string $message])
— compare arrays ignoring specified keys
assertArrayIsEqualToArrayOnlyConsideringListOfKeys(...)assertArrayIsEqualToArrayIgnoringListOfKeys(...)
Object
assertObjectHasProperty(string $propertyName, object $object[, string $message])assertObjectNotHasProperty(...)
File
assertFileExists(string $filename[, string $message])assertFileDoesNotExist(...)assertFileEquals(string $expected, string $actual[, string $message])assertFileNotEquals(...)assertFileIsReadable(...)/assertFileIsWritable(...)assertDirectoryExists(...)/assertDirectoryDoesNotExist(...)
JSON
assertJson(string $actualJson[, string $message])— valid JSONassertJsonStringEqualsJsonString(string $expectedJson, string $actualJson[, string $message])assertJsonStringEqualsJsonFile(string $expectedFile, string $actualJson[, string $message])assertJsonFileEqualsJsonFile(string $expectedFile, string $actualFile[, string $message])
XML
assertXmlStringEqualsXmlString(...)/assertXmlStringEqualsXmlFile(...)assertXmlFileEqualsXmlFile(...)
Exception Expectations
Called before the code that should throw:
$this->expectException(string $className)— expects exception of given class$this->expectExceptionMessage(string $message)— message contains substring$this->expectExceptionMessageMatches(string $regex)— message matches regex$this->expectExceptionCode(int|string $code)— exception code matches$this->expectExceptionObject(Throwable $exception)— matches class + message + code
Error / Warning / Deprecation Expectations
$this->expectUserDeprecationMessage(string $message)— expectsE_USER_DEPRECATED$this->expectUserDeprecationMessageMatches(string $regex)
Constraint System
assertThat(mixed $value, Constraint $constraint[, string $message]) accepts any PHPUnit\Framework\Constraint\Constraint. Use for custom assertions:
$this->assertThat($value, $this->logicalAnd(
$this->greaterThan(5),
$this->lessThan(10),
));Logical combinators: logicalAnd(...), logicalOr(...), logicalNot(Constraint), logicalXor(...).
Built-in constraints: isTrue(), isFalse(), isNull(), isEmpty(), isInstanceOf(), equalTo(), identicalTo(), greaterThan(), lessThan(), matchesRegularExpression(), stringContains(), arrayHasKey(), containsIdentical(), callback(callable).
Configuration Reference
PHPUnit is configured via phpunit.xml (or phpunit.xml.dist for version-controlled defaults). The XML schema is version-specific: https://schema.phpunit.de/11.5/phpunit.xsd.
Minimal Configuration
<?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">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>Key <phpunit> Attributes
- `bootstrap` (default: —) — Autoloader script path
- `colors` (default:
false) — Colored output - `cacheDirectory` (default: —) — Cache dir for test results and coverage analysis
- `cacheResult` (default:
true) — Cache test results for defect/duration ordering - `executionOrder` (default:
default) — Test order:default,random,depends,random, etc. - `stopOnDefect` (default:
false) — Stop on first error/failure/warning/risky - `stopOnFailure` (default:
false) — Stop on first failure - `failOnWarning` (default:
false) — Treat warnings as CI failures - `failOnRisky` (default:
false) — Treat risky tests as CI failures - `failOnDeprecation` (default:
false) — Treat deprecations as CI failures - `failOnNotice` (default:
false) — Treat notices as CI failures - `beStrictAboutTestsThatDoNotTestAnything` (default:
true) — Mark assertionless tests risky - `beStrictAboutOutputDuringTests` (default:
false) — Mark tests with output risky - `beStrictAboutCoverageMetadata` (default:
false) — Mark tests without coverage attrs risky - `processIsolation` (default:
false) — Run each test in separate PHP process - `backupGlobals` (default:
false) — Backup/restore global variables per test - `backupStaticProperties` (default:
false) — Backup/restore static properties per test
<testsuites> Element
Define named test suites for selective execution (--testsuite unit):
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory>tests/Integration</directory>
<exclude>tests/Integration/Legacy</exclude>
</testsuite>
</testsuites>Discovery: files matching *Test.php in specified directories.
<source> Element (PHPUnit 11+)
Replaces the old <coverage><include> filter. Controls both code coverage filtering and deprecation/notice/warning source attribution.
<source restrictDeprecations="true"
restrictNotices="true"
restrictWarnings="true">
<include>
<directory suffix=".php">src</directory>
</include>
<exclude>
<directory suffix=".php">src/Migrations</directory>
<file>src/Kernel.php</file>
</exclude>
</source>restrictDeprecations="true"— only report deprecations from your source coderestrictNotices="true"— only report notices from your source coderestrictWarnings="true"— only report warnings from your source code
Migration from PHPUnit 10: move <include>/<exclude> from <coverage> to <source>. <coverage> filter is deprecated in 11, removed in 12.
<coverage> Element
Controls code coverage report generation (not filtering — use <source> for that):
<coverage cacheDirectory=".phpunit.cache/coverage"
includeUncoveredFiles="true">
<report>
<html outputDirectory="build/coverage"/>
<clover outputFile="build/logs/clover.xml"/>
<text outputFile="php://stdout"/>
</report>
</coverage>Report types: html, clover, cobertura, crap4j, text, php.
<php> Element
Set PHP ini values and environment variables for the test run:
<php>
<ini name="memory_limit" value="-1"/>
<ini name="error_reporting" value="-1"/>
<env name="APP_ENV" value="test"/>
<env name="DB_DATABASE" value="testing" force="true"/>
<var name="FIXTURE_DIR" value="tests/fixtures"/>
</php><extensions> Element
Register PHPUnit extensions:
<extensions>
<bootstrap class="Vendor\Extension\Bootstrap">
<parameter name="key" value="value"/>
</bootstrap>
</extensions><groups> Element
Include or exclude test groups:
<groups>
<include>
<group>unit</group>
</include>
<exclude>
<group>slow</group>
</exclude>
</groups>Test Execution Order
Configure via executionOrder attribute:
default— declaration orderrandom— random order (detects hidden dependencies)depends,random— respect#[Depends], randomize the restdepends,defects— defects first, then by dependencydepends,duration— fastest first, then by dependency
Recommended Strict Configuration
<phpunit
beStrictAboutTestsThatDoNotTestAnything="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutCoverageMetadata="true"
failOnWarning="true"
failOnRisky="true"
failOnDeprecation="true"
failOnNotice="true">Data Providers Reference
Data providers supply arguments to test methods. Each dataset runs the test once with those arguments, producing independent test results.
Declaration Rules
- Must be `public static` — non-static providers are removed in PHPUnit 11
- Must not start with `test` — PHPUnit treats
test*methods as test methods - Return type:
array,Iterator,Generator, or anyiterable - Each iteration yields an array of arguments matching the test method parameters
- Connect to test methods via
#[DataProvider('methodName')]attribute @dataProviderannotation is deprecated in PHPUnit 11, removed in 12
Basic Pattern (Array)
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
final class MathTest extends TestCase
{
#[DataProvider('additionCases')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public static function additionCases(): array
{
return [
[0, 0, 0],
[1, 2, 3],
[-1, 1, 0],
];
}
}Named Datasets
Use string keys for readable failure output:
public static function additionCases(): array
{
return [
'zeros' => [0, 0, 0],
'positive sum' => [1, 2, 3],
'cancel out' => [-1, 1, 0],
];
}Failure output: testAdd with data set "cancel out" (-1, 1, 0).
Generator / Yield Providers
For large datasets or lazy generation:
public static function largeCases(): Generator
{
yield 'small input' => [1, 1, 2];
yield 'boundary' => [PHP_INT_MAX, 0, PHP_INT_MAX];
foreach (range(1, 100) as $i) {
yield "generated #{$i}" => [$i, $i, $i * 2];
}
}External Providers
Use #[DataProviderExternal] for providers in a separate class:
use PHPUnit\Framework\Attributes\DataProviderExternal;
#[DataProviderExternal(MathDataSets::class, 'additionCases')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}The external class method must be public static.
Multiple Providers
Stack multiple attributes — datasets from all providers are combined:
#[DataProvider('positiveNumbers')]
#[DataProvider('negativeNumbers')]
public function testAbsoluteValue(int $input, int $expected): void
{
$this->assertSame($expected, abs($input));
}Inline Data with #[TestWith]
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);
}Constraints and Gotchas
- All providers execute before any test runs — including providers for filtered-out tests. Keep providers
lightweight.
- No mock objects in providers — mock creation requires the test framework to be initialized, which hasn't happened
during provider execution
- Data should be scalar/immutable — providers should return scalars, value objects, or stubs. Never create service
objects or complex graphs in providers.
- Empty providers are forbidden in PHPUnit 11 — an
InvalidDataProviderExceptionis thrown if a provider returns no
data
- Duplicate named keys trigger
InvalidDataProviderException - No code coverage is collected during provider execution
TestDox Integration
Use parameter names as placeholders in #[TestDox]:
#[DataProvider('additionCases')]
#[TestDox('Adding $a to $b results in $expected')]
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}$_dataName holds the dataset key name.
Test Doubles Reference
PHPUnit distinguishes stubs (control return values) from mocks (verify interactions). Use createStub() for stubs. Use createMock() for mocks with expectations.
Creation Methods
createStub(string $type): Stub
Returns a stub for an interface or extendable class. All methods return auto-generated values matching their return type. Configure return values with willReturn() etc.
Do not call `expects()` on stubs — triggers deprecation in PHPUnit 11, error in 12.
$stub = $this->createStub(Repository::class);
$stub->method('find')->willReturn(new Entity(id: 1));createConfiguredStub(string $type, array $config): Stub
Convenience wrapper — configures multiple methods at once:
$stub = $this->createConfiguredStub(Cache::class, [
'get' => 'cached-value',
'has' => true,
]);createMock(string $type): MockObject
Returns a mock object. Same defaults as createStub() but supports expects() for invocation verification.
$mock = $this->createMock(Logger::class);
$mock->expects($this->once())
->method('log')
->with('error', $this->stringContains('failed'));createConfiguredMock(string $type, array $config): MockObject
Like createConfiguredStub() but returns a mock that supports expectations.
createStubForIntersectionOfInterfaces(array $interfaces): Stub
Creates a stub satisfying an intersection type A&B:
$stub = $this->createStubForIntersectionOfInterfaces([Readable::class, Countable::class]);createMockForIntersectionOfInterfaces(array $interfaces): MockObject
Same as above but returns a mock with expectation support.
Return Value Configuration
Chain after ->method('methodName'):
- `willReturn($v1, $v2, ...)` — Returns values in sequence; last value repeats
- `willReturnArgument(int $index)` — Returns the nth argument unchanged
- `willReturnSelf()` — Returns the stub/mock itself (fluent interfaces)
- `willReturnCallback(callable $cb)` — Delegates to callback
- `willReturnMap(array $map)` — Maps
[arg1, arg2, ..., returnValue]arrays - `willThrowException(Throwable $e)` — Throws exception
Consecutive returns
$stub->method('fetch')->willReturn('first', 'second', 'third');
// Call 1 → 'first', Call 2 → 'second', Call 3+ → 'third'Invocation Matchers (Mocks Only)
Use with $mock->expects($matcher):
- `$this->any()` — Zero or more calls
- `$this->never()` — Never called
- `$this->once()` — Exactly one call
- `$this->atLeastOnce()` — One or more calls
- `$this->exactly(int $n)` — Exactly n calls
- `$this->atMost(int $n)` — At most n calls
Argument Constraints
Chain ->with(...) after ->method():
$mock->expects($this->once())
->method('save')
->with(
$this->isInstanceOf(User::class),
$this->greaterThan(0),
);Any PHPUnit\Framework\Constraint\Constraint works as a with() argument. Use $this->callback(fn ($arg) => ...) for custom matching.
`withConsecutive()` was removed in PHPUnit 10. For consecutive argument verification, use willReturnCallback() with manual tracking or multiple expects() calls.
MockBuilder (Advanced)
Use when createStub()/createMock() defaults don't suffice:
$mock = $this->getMockBuilder(Service::class)
->onlyMethods(['process']) // Only double these methods
->disableOriginalConstructor() // Skip constructor
->getMock();Key MockBuilder methods (non-deprecated in PHPUnit 11):
onlyMethods(array $methods)— specify which methods to doublesetConstructorArgs(array $args)— pass constructor argumentsdisableOriginalConstructor()/enableOriginalConstructor()disableOriginalClone()/enableOriginalClone()setMockClassName(string $name)— custom class name for the doubledisableAutoReturnValueGeneration()— require explicit return configgetMock()— terminal call, returns the configured mock
Deprecated in PHPUnit 11 (removed in 12):
addMethods()— use interfaces oronlyMethods()insteadgetMockForAbstractClass()— test concrete classes insteadgetMockForTrait()— test classes using the trait insteadenableArgumentCloning()/disableArgumentCloning()allowMockingUnknownTypes()/disallowMockingUnknownTypes()enableProxyingToOriginalMethods()/setProxyTarget()
Limitations
final,private, andstaticmethods cannot be doubledenumtypes arefinaland cannot be doubled- Favour doubling interfaces over classes — better design, fewer limitations
- Mock objects cannot be created in data provider methods
PHP 8.4 Property Hooks
For interfaces with get/set-hooked properties:
use PHPUnit\Framework\MockObject\Runtime\PropertyHook;
$stub->method(PropertyHook::get('name'))->willReturn('value');