Frequently Asked Questions
Answers to questions I am asked again and again about PHPUnit, test automation, code coverage, and testing in PHP. Where it helps, an answer links to a fuller article.
Code Coverage
Should I use PCOV or Xdebug to collect code coverage?
Use PCOV when you only need line coverage: it is considerably faster and ideal for CI. Reach for Xdebug when you need branch or path coverage, or when you already run it for step debugging. Enabling both at once rarely makes sense.
Read more in the article: PCOV or Xdebug?
Can I merge code coverage data from several test runs?
Yes. Have each run write its data to a separate .cov file with --coverage-php, for example when you shard or parallelise your suite, and then combine those files into one report with the merge command of PHPCOV. Since PHPUnit 13.1 and PHPCOV 13.0 the files contain relative paths. This is why it also works when the runs happened on different machines, or partly with the PHAR and partly with a Composer-installed PHPUnit. With --include-git-information, PHPCOV also checks that all files belong to the same state of the code.
Read more in the article: Merging code coverage data
My coverage is green but bugs still slip through. What now?
Line coverage only tells you which code was executed, not whether your assertions actually check anything. Path coverage reveals untested combinations of branches, and mutation testing measures whether your tests would even notice a change to the code. Both show whether the confidence that coverage gives you is justified.
Read more in the article: Path Coverage or Mutation Testing?
Should we enforce a minimum code coverage?
Not as a target imposed from outside. As soon as a team has to reach 80, 90 or even 100 % because a requirement, a dashboard or a bonus depends on it, people optimise the metric instead of what it was meant to measure. The trivial parts get tested because they are easy to cover, and tests call methods without checking their results. PHPUnit catches the crudest form of this: a test without assertions is marked as risky, and its code coverage data is discarded. Weak assertions are only exposed by mutation testing. No tool, however, can make the decision for you to treat a metric as an indicator and not as a target.
Read more in the article: When the metric becomes the target
How do I see what my unit tests cover on their own?
Declare the size of your tests with the #[Small], #[Medium] and #[Large] attributes. Since PHPUnit 13.3 the HTML code coverage report can be filtered by these sizes, and every number on the page is recalculated. A class can show 100 % coverage overall even though only a fraction of it comes from small tests and the rest is only reached through large end-to-end tests. One click on Small shows you what your unit tests cover on their own. The difference between All and the combination of the three sizes also shows you how much of your coverage comes from tests that declare no size at all.
Read more in the article: Repetition and Insight: What is new in PHPUnit 13.3?
Test Doubles (Mocks and Stubs)
What should I use instead of withConsecutive()?
withConsecutive() was removed in PHPUnit 10. PHPUnit 13 introduced two rules that replace it: withParameterSetsInOrder() expects the parameter sets in exactly the order given, and withParameterSetsInAnyOrder() only expects all of them to be used. PHPUnit 13.2 added withParameterSetsInPartialOrder() for the case in between, where you pin some parameter sets to fixed positions and leave the rest unordered. Require the strict order only where it is intrinsic to correctness. Otherwise your test fails as soon as somebody reorders the calls without changing the observable behaviour. Whoever is still on PHPUnit 10, 11 or 12 has to keep using a willReturnCallback() that checks the arguments itself.
Read more in the article: Better than withConsecutive()
When should I use a stub and when a mock?
Use a stub when you only need a collaborator to return canned values so the code under test can run. Use a mock when the interaction itself is what you want to verify. Verifying calls that do not matter couples your tests to the implementation and makes them brittle.
Read more in the article: The Stub/Mock Intervention
PHPUnit reports that no expectations were configured for my mock object. What now?
PHPUnit has reported this since version 12.5 whenever a test creates a mock object with createMock() but configures no expectations on it. Either you forgot the expectations and should add them, or you do not need a mock object at all and should create a test stub with createStub() instead. The distinction is deliberate: createStub() tells whoever reads the test that a dependency is being decoupled, createMock() tells them that the communication between objects is being verified. Since PHPUnit 12, expectations can no longer be configured on a test stub at all.
Read more in the article: Testing with(out) dependencies
The any() matcher is deprecated. How do I migrate?
Ask yourself what you need the test double for. If the call matters, replace any() with a specific matcher such as once() or exactly(2). If it does not matter, you do not need a mock object at all: create a test stub with createStub(). expects($this->any()) says that you want to verify communication but do not care whether it happens, and that contradiction is the reason for the deprecation.
Read more in the article: From anti-pattern to clarity
Should I replace value objects and DTOs with test doubles?
No. A value object stands for a fixed value, so there is nothing to isolate the code under test from. Use real instances and you test against the same behaviour that production gets. The same applies to DTOs, especially when you design them to be immutable.
Read more in the article: Testing with DTOs and Value Objects
Performance and Debugging
How can I make my PHPUnit test suite faster?
There are four levels to work through: the test runner configuration, the infrastructure your tests touch, parallelisation, and the design of the tests themselves. The biggest and most lasting gains almost always come from better test design, not from throwing more cores at a slow suite.
Read more in the article: Turbo-Charging Your PHPUnit Suite
How do I find out which tests slow my suite down?
PHPUnit knows how long every test took, how much CPU time it used, and how much memory it needed. With --log-otr it writes that data into an Open Test Reporting log file. The otr-report tool reads the file back and lists the slowest tests, optionally only those above the mean runtime. That is almost always more honest than intuition.
Read more in the article: What your test run already knows
How do I find performance problems in PHP?
Measure before you change anything. A profiler shows you where time and memory actually go, which is regularly somewhere other than where you suspected. Guessing and optimising by intuition tends to make the code more complex without making it faster.
Read more in the article: Debugging Performance in PHP
How do I make my software easier to debug?
By making it explicit at every level. Components with defined interfaces let you narrow down where a bug is; structured logging, metrics and tracing show what happens inside; deterministic builds and versioned configuration let you reproduce the state in which the bug occurred. In the design, little hidden state helps, and so does failing fast: an exception at the point where invalid data arrives is easier to trace than a failure three calls later. In the code, meaningful names, small methods, guard clauses and error messages that name the values involved do the rest. With event sourcing, you also get the complete history of how the system reached its state, and you can replay it up to the event that caused the problem.
Read more in the article: Debugging as a design goal
Should I enable OPcache for my test suite?
Yes. On the command line OPcache is off by default, because opcache.enable_cli is 0, so your test suite runs on unoptimised bytecode until you change that. Despite its name, OPcache also optimises: fifteen passes rebuild the bytecode between the compiler and the executor, and without OPcache none of them run. For the test suite I measured this on, that cost about 14 %: 9.91 seconds instead of 8.48 seconds for one run. The optimiser itself added about 0.15 seconds per invocation.
Read more in the article: How PHP optimises bytecode
Is the JIT compiler worth it for my test suite?
That depends on where the time goes and which mode you choose. If it goes into the database, the file system or the network, the JIT does nothing for you. If your PHP code does a lot of computation itself, it can pay off: for the test suite of a raytracer with 177 tests, opcache.jit=tracing brought one run down from 9.16 to 6.48 seconds. With opcache.jit=function, the same run took 5.8 % longer than without any JIT, because the function JIT compiles everything before anything runs, including code that is never executed. The JIT is not switched on in any default installation, so you have to set opcache.jit yourself and enable OPcache for the command line. Xdebug and the JIT exclude each other: once Xdebug hooks into execution, the JIT switches itself off.
Read more in the article: How PHP generates machine code
Why is the same PHP version faster on one machine than on another?
One reason is that the compiler PHP was built with decides the dispatch model of the executor: how it gets from one instruction to the next. Until PHP 8.5, a PHP built with Clang was between 2.8 % and 44 % slower than one built with GCC, depending on the benchmark, at the same version number and running the same PHP code. The TAILCALL model introduced in PHP 8.5 closed that gap for Clang 19 and newer on x86-64 and ARM64. Since PHP 8.5, the ZEND_VM_KIND constant tells you which model your binary uses. When you compare numbers between two installations, it belongs next to the version number, as do opcache.optimization_level and opcache.jit.
Read more in the article: How PHP executes bytecode
Test Design and Structure
Do I need setUp() to prepare my test fixture?
No. setUp() runs before every test in the class, including the tests that never touch the fixture it creates. Once inheritance is involved, a forgotten parent::setUp() call quietly disables whatever the parent class provides. I write private methods in the test class that create the objects instead, and call them only from the tests that need them. The test holds no reference afterwards, and each test makes its own dependencies visible.
Read more in the article: How I manage test fixture
My project has no tests and an upgrade is due. Where do I start?
With characterization tests. They document what the code actually does without judging whether that behaviour is right: write a test with an assertion you know will fail, read the actual result off the failure message, and put that value into the assertion. Repeat with different inputs until you have enough coverage. That gives you the safety net you need before you break the code into smaller, testable units.
Read more in the article: A flight recorder for your code
Can I put the data for a data provider right at the test method?
Since PHPUnit 13.1 you can, with the #[DataProviderClosure] attribute. Instead of referencing a separate method by its name as a string, you define the data provider as a closure right at the test method. The closure must be static and, like a conventional data provider method, return an array or a Traversable with the argument sets. This requires PHP 8.5, because closures in constant expressions were not allowed before.
Read more in the article: Closures, Clarity, and Control: What is New in PHPUnit 13.1?
Data providers or property-based testing?
Both run one test method against many inputs, but they answer different questions. A data provider records concrete examples: given this input, expect exactly this result. A property states something that must hold for all inputs and leaves the examples to a generator. Data providers document what you already know; properties expose the gaps in it.
Read more in the article: Data Provider or Properties?
What is property-based testing?
Instead of checking individual examples, you state a property that should hold for all valid inputs and let a tool generate many of them, including edge cases you would not think of. When it finds a failure, it shrinks the input to the smallest case that still breaks.
Read more in the article: Property-Based Testing
What does shrinking do in property-based testing?
It turns a random input for which a property does not hold into the smallest counterexample for which it still does not hold. Without shrinking, the test might tell you that your code fails for [-12, 5, -7, 3]. With shrinking, that becomes [-1], and it is immediately obvious that the assumption that a sum is never negative was wrong in the first place. Every generator has a shrinker that knows how to make its values smaller. Eris comes with sensible defaults for common types, so there is nothing to configure.
Read more in the article: Smaller input, greater insight
How does a test know what is correct?
Whatever decides that is the test oracle. For a return value, the oracle is the comparison with the expected value. For a state change it is the check you run after the call, and for communication with collaborators it is the expectations you configured on your mock object. If you cannot say what the oracle in a test is, that test may not be checking anything.
Read more in the article: Seeing the Truth: Test Oracles
How do I test a system that uses event sourcing?
You stop checking the state after an operation and check which events it emitted instead. The Given, When, Then pattern fits this exactly: Given are the events that have already happened, When is the command you execute, and Then are the events you expect or the exception that should be raised. The domain events from Event Storming become the blueprint for your tests. A project-specific base class for test cases can manage the event store and even generate documentation in Event Storming notation from the same three phases.
Read more in the article: From Events to Insights
How do I test a new version of my event-sourced system against real data?
With replay testing: you play back the entire history from production in a test environment, but with the new code. If that runs without errors, you know the new code copes with every edge case of the past years. It will not find logic errors, though. For those, you also compare the result with the state from production, such as a snapshot of an aggregate or a read model. Everything that leaves the system boundary has to be replaced with dummies, and reactions to events must not issue new commands, or you generate events that never happened. The clock, random values and responses from external APIs have to be made deterministic.
Read more in the article: Replay Testing
Do I have to change existing tests when I fix a bug?
Ideally not. The best bug fix adds a regression test and rewrites no existing expectation: the new test proves the bug is gone, the untouched ones prove that nothing else broke. If a fix does force you to rewrite an existing expectation, stop and look. Usually one of three patterns applies: the test was coupled to implementation details, the fix is really a behaviour change and should be communicated as one, or you are re-arming a test that was silenced earlier.
Read more in the article: Untouched tests are half the proof
How do I build type-safe collections in PHP?
Wrap an array in a dedicated class that only accepts the type you want, expose the operations you actually need, and let static analysis verify the element type through generics annotations. You gain safety and a clear API instead of passing raw arrays around.
Read more in the article: Type-Safe Collections
Running Tests
How do I track down flaky tests?
By running the same test many times in a row. Since PHPUnit 13.3, --repeat 100 runs every individual test up to one hundred times and skips the remaining repetitions as soon as one fails. The #[Repeat] attribute does the same for individual test methods. When a flaky test cannot be fixed, for example because it depends on network services, #[Retry(3)] runs it up to three times. Every test that only passed after a failed attempt is listed in the summary of the test run, and that cannot be disabled. Only test methods that declare void as their return type and do not depend on other tests are repeated or retried.
Read more in the article: Repetition and Insight: What is new in PHPUnit 13.3?
How do I run exactly one specific test?
Since PHPUnit 13.2, with test IDs. A test ID names a single test unambiguously, including the data set for a test that uses a data provider. --list-test-ids prints every test in your suite as a test ID, and --run-test-id runs exactly the test that the ID you pass refers to. Unlike --filter, this uses exact matching instead of a regular expression that can accidentally match more tests than intended, or none at all. --test-id-filter-file takes a file with one test ID per line, for example to re-run only the tests that failed last time or to shard a large suite across several workers.
Read more in the article: Precision and Clarity: What is new in PHPUnit 13.2?
Static Analysis, Coding Standards and Code Reviews
Psalm or PHPStan?
I use PHPStan, mostly for its large ecosystem of extensions for frameworks and libraries, and it is what I recommend to anyone new to static analysis. Running both tools on the same code is not something I recommend: their rules can contradict each other, and you end up trying to satisfy two different opinions about good code. Which tool you pick matters far less than deciding to use static analysis at all.
Read more in the article: Psalm or PHPStan?
PHP_CodeSniffer or PHP-CS-Fixer?
For coding standards I use PHP-CS-Fixer: everything this tool can complain about it can also fix automatically. Locally I let it fix the code before a commit, in CI it runs in check mode and only reports deviations. I still use PHP_CodeSniffer where I analyse code rather than format it, for due diligence, security audits, and guided refactoring, with my own sniffs and rule sets such as PHPCompatibility.
Read more in the article: PHP_CodeSniffer or PHP-CS-Fixer?
Are tests enough, or do I also need static analysis?
No single technique is enough. Static analysis finds type errors and inconsistencies without running the code. Unit tests check specific behaviour, property-based testing generalises it, mutation testing measures how good your tests are, and fuzzing explores what nobody thought of. Each layer covers a different slice of the infinitely many ways code can be wrong.
Read more in the article: Everything we have
Do types make tests unnecessary?
No. Types do not replace tests, but they make code rigid, and what a type guarantees you do not have to test. Where the return type makes the promise, neither a cast nor a test for it is needed. Mutation testing shows you such places: a surviving mutant can mean that a test is too shallow, or that the code it changes does nothing at all, such as an (int) cast on the return value of count(). Since Infection 0.30, --static-analysis-tool=phpstan runs PHPStan over the mutants that escaped the tests and kills those that violate the declared types.
Read more in the article: Where tests end and types begin
Which code review strategy fits my team?
That depends on two questions: does the review happen while the code is written or afterwards, and does the change wait for it? Pair and ensemble programming review synchronously and before the merge; they pay off for critical changes, risky refactorings and onboarding. Pull requests review asynchronously but still block the merge. That suits distributed teams, at the price of waiting times and misunderstandings in written comments. Reviewing after the push gives you the most speed, but only with small commits, feature flags or branch by abstraction, comprehensive automated tests and changes you can roll back quickly. Whichever you choose, review architecture, design decisions and comprehensibility rather than formatting.
Read more in the article: Effective Code Reviews
Security
What is Test-Driven Security?
For almost every vulnerability that reaches production, there is a test which, had it existed, would have prevented it. Test-Driven Security treats known weakness categories as a checklist and uses the testing tools you already have to prove that each class of flaw is absent.
Read more in the article: Test-Driven Security
Can a slow test suite be a security risk?
It can. If your suite is too slow to run often, automated tools and AI agents cannot use it to check their changes either, so vulnerabilities stay hidden longer.
Read more in the article: Speed as a security feature
Is it safe to install PHPUnit in production?
No. PHPUnit is a development dependency: require it under require-dev and make sure it never ends up in your production deployment or autoloader. Shipping test tooling to production needlessly enlarges your attack surface.
Read more in the article: PHPUnit: A Security Risk?
Does static analysis really never execute my code?
Usually it does not, but that is not guaranteed. PHPCSUtils had a vulnerability (CVE-2026-65954) where a sniff passed the source text of an array key to eval(). A prepared array key was therefore executed while the file was being analysed, for example in the CI job that checks every pull request. Keep your analysis tools up to date. disable_functions does not help here: eval() is a language construct and has no entry in the function table.
Read more in the article: When static analysis runs your code
Do I need fuzzing when my tests already cover all of the code?
For code that processes input from untrusted sources, yes. My csv-parser library had 100 % line and branch coverage, over 70 % path coverage and a covered code mutation score of 100 %. PHP-Fuzzer still found a bug: numeric strings larger than a 64-bit integer can hold were not parsed correctly. Property-based testing needs explicitly stated properties and runs as fast as unit tests. A coverage-guided fuzzer gets by with the implicit oracle “does not crash”, but it has to run for hours or days to get there.
Read more in the article: Security through chaos
Someone reported a vulnerability in my package. What now?
Keep the details private until a fixed version exists, and reply to the reporter quickly, even if only briefly. Treat the report as a claim and check it: which weakness it describes, who can trigger it, what can be achieved with it and which versions are affected. Never run proof-of-concept code on your development machine, which holds your SSH keys and credentials; use a sandbox, a container or a throwaway VM, and turn the reproducer into a failing test. Prepare the fix in a temporary private fork, publish the release first and the GitHub Security Advisory shortly afterwards, and state the affected versions as precise Composer constraints: since Composer 2.9, a range that is too broad makes unaffected versions uninstallable. The PHP Foundation's Ecosystem Security Team helps with triage and coordinated disclosure.
Read more in the article: So you received a security report. Now what?
Supply Chain Security
How do I stop a dependency with a known vulnerability from being installed?
Since Composer 2.9 the resolver does it for you: versions with known security advisories are no longer considered at all, by default, every time the dependency graph is resolved. Before that, composer audit only informed you after the fact, and a hard guarantee required a package such as roave/security-advisories. Expect this to reach you in places where the passive audit merely informed you: an install can now fail because a vulnerable version is no longer resolvable.
Read more in the article: The Bouncer in the Dependency Resolver
Does Composer also protect me from compromised packages?
Since Composer 2.10, to a large extent, and without any configuration. Composer blocks versions flagged as malware not only during composer update but also during composer install. A malicious version that is flagged only after your composer.lock was generated therefore fails on the next install instead of silently reaching CI or production. On Packagist, the Git reference of a stable version can no longer be changed after the fact since July 2026. That is exactly what nearly every recent attack on Packagist relied on: with compromised GitHub access, attackers moved trusted tags to malicious commits.
Read more in the article: Composer and Packagist under supply chain stress
I tagged a broken release. Can I delete the tag and set it again?
No, and that was never safe. Automated systems react to changes on Packagist within seconds, mirrors cache the archive under the original reference, and CI systems with automated dependency updates may already have committed the version to a lock file. Since 7 July 2026, Packagist blocks retagging a stable version, shows a warning on the package page and notifies all maintainers by email. Version numbers cost nothing: the fix belongs in the next patch version, with a fourth component if need be, so 3.2.0 becomes 3.2.0.1. And if you maintain packages on Packagist and have not enabled multi-factor authentication yet, do it now.
Read more in the article: Composer and Packagist under supply chain stress
How do I create a reliable SBOM for my PHP project?
From what you declared, not from what a scanner finds. The open source attribution for the MG4 shows where scanning leads: it lists an Android botnet, in all likelihood the unchecked hit of a code scanner. If you manage your dependencies exclusively with Composer, your composer.lock already lists every component with its exact version, secured by hashes. The Composer plugin cyclonedx-php-composer turns that into an SBOM in CycloneDX format with SPDX license identifiers and package URLs. Do not copy a library into your repository past Composer: neither the blocking of known vulnerabilities nor the malware policy applies to such a dependency, and composer audit does not report it. The PHPUnit PHAR discloses its own contents with --manifest, --sbom and --composer-lock.
Read more in the article: Your composer.lock knows what a carmaker only guesses
How do I find out whether a release I already shipped is affected by a new vulnerability?
Only if you look. composer install from a lock file does not block versions with security advisories, and that is deliberate: a lock file exists so that the same thing is installed twice. A release you shipped in 2024 therefore keeps its vulnerable dependencies, and nothing in your daily work tells you about it. Keep composer.json and composer.lock of every release you still support, and run composer audit --locked --no-dev against them on a schedule, from cron or as a scheduled CI job. The same command works as a release gate in your pipeline, because an advisory you have not handled makes it exit with a non-zero code.
Read more in the article: What ISO/IEC 18974 asks of your composer.lock
How do I harden my GitHub Actions workflows?
Treat a workflow as code that touches secrets. The most common and most dangerous mistake is interpolating a ${{ ... }} expression into a shell script: the value is substituted before the shell ever reads the script, so a branch name freely chosen in a fork becomes executed code. Pass such values through environment variables and quote them in the script. A static analyser for workflow files such as zizmor finds this and related weaknesses in minutes.
Read more in the article: Hardening GitHub Actions workflows
Why should I delete unused branches?
Because every branch that accepts pull requests and is connected to a pipeline is a target for Poisoned Pipeline Execution. A harmless-looking pull request brings along a modified CI configuration, an altered build script, or a file that the pipeline processes. For PHPUnit, security advisory GHSA-qrr6-mg7r-m243 showed in April 2026 how a phpunit.xml modified in a pull request can inject INI directives such as auto_prepend_file into child processes. Branch protection rules, ephemeral runners and time-limited credentials remain necessary. But a branch that does not exist cannot receive a pull request carrying such an attack.
Read more in the article: The attack surface begins in the repository
Versions and Migration
My tests stopped working after a PHPUnit update. What now?
A new major version of PHPUnit is released every year on the first Friday of February and is where the cleanup happens. What it changes is announced a year earlier, with the release of the previous major version. Read that announcement and schedule the necessary changes across the year, and the upgrade holds no surprises. Do not let a testing tool update itself to a new major version unattended.
Read more in the article: Help! My tests stopped working
What does it mean when a PHPUnit feature is deprecated?
PHPUnit distinguishes two levels. A soft deprecation produces no output during a test run, but IDEs and static analysis tools can already warn about it. A hard deprecation is reported on every test run. Your tests keep working in both cases, and a hard-deprecated feature is removed no earlier than the next major version.
Read more in the article: From anti-pattern to clarity
expectExceptionMessage() is deprecated. What should I use instead?
The method was misleadingly named: it never required the exception message to be equal to the expected string, only to contain it. PHPUnit 13.2 therefore introduced an explicit API. expectExceptionMessageIs() requires an exact match, expectExceptionMessageIsOrContains() keeps the familiar substring behaviour, and expectExceptionMessageMatches() remains available for regular expressions. If you want to migrate without changing how your tests behave, expectExceptionMessageIsOrContains() is the direct replacement.
Read more in the article: Precision and Clarity: What is new in PHPUnit 13.2?
AI in Software Development
The AI agent's tests are green. Can I merge the code?
Green tests show that the code does what the tests check, not that it does the right thing. I had an AI agent implement a non-trivial software metric. It took fifteen minutes; judging whether the implementation was correct then took me hours. Writing code was never the bottleneck: understanding the problem, designing the solution, and checking the result are the hard parts.
Read more in the article: Faster than understanding
How do I gain confidence in code that an AI agent wrote?
Not from the signals the agent produced itself. When the same agent writes the implementation and the tests, green tests only show that the implementation agrees with itself. I call that borrowed confidence. Earned confidence needs independent evidence. For an AI-generated implementation of the ACPATH software metric, I use three sources of it. A naive path counter that is obviously correct serves as an oracle, and I compare the implementation against it on thousands of generated functions. I derive metamorphic relations from the paper rather than from the code. And mutation testing shows whether the generated tests even notice defects that I introduce. This is slower than accepting the output of the agent, but much faster than becoming a domain expert first.
Read more in the article: Confidence is not transferable
What does my project need before AI agents can work on it usefully?
The same things that help human teams, only less optional: a test suite you trust, static analysis, maintained documentation, and code review. An agent can analyse the codebase, write an implementation and its tests, run them, and prepare a pull request with nobody watching. Those practices are the frame that makes such autonomy reviewable.
Read more in the article: Beyond Best Practices
When is it better not to write code at all?
Whenever you have not understood the problem yet. A line you do not write is a line nobody has to debug, maintain, test, or understand again in five years. That applies with particular force to code an AI agent produces in minutes: if you cannot judge whether it is correct, your maintenance burden grows without you knowing what you got for it.
Read more in the article: Code I do not have does not cause any problems
What should my tool's output look like now that coding agents read it?
Your tool is now read in two ways: by a person looking at a rendered terminal and by a coding agent reading the raw bytes. Every difference between the two can be exploited. Strings from input should not reach the output unchanged: PHPUnit replaces control and bidirectional characters with visible \u{NNNN} escapes, so that escape sequences cannot make a failure look like a pass. Input must not be able to forge the structure of the output either, and an invocation must not be able to hang: PHPUnit 13.4 added --timeout, which stops a run cleanly, writes the logs and exits with code 124. Above all, provide a verdict that does not require parsing prose, such as the exit code and a machine-readable log.
Read more in the article: Your tool has a new reader
PHP and Open Source
What is the best way to report a bug in PHPUnit?
With a failing test. It states which call, with which arguments, produces which wrong result. I can run it in seconds instead of having to understand a diff. It adds one expectation and changes none, and once the bug is fixed, it becomes the regression test. A pull request that contains nothing but a failing test is a contribution I am glad to receive. The contribution guidelines of PHPUnit also require that a human is accountable for every contribution and discloses how a coding assistant was used, if one was.
Read more in the article: When the diff is free
Why can I not use two versions of the same library at the same time?
Because the PHP engine allows exactly one definition of every class name per process. Declaring the same name a second time is a fatal error. Composer therefore resolves every package to exactly one version and reports a conflict when the version constraints cannot be reconciled. That has a welcome side effect: the error reaches the maintainers who can widen a constraint that is too tight, which is the main reason why PHP dependency trees stay small. The PHPUnit PHAR cannot take part in this arrangement, because it brings its dependencies into a process whose dependency tree it has never seen. Since PHPUnit 8.3 it therefore renames them during the build with php-scoper, so SebastianBergmann\Exporter becomes PHPUnitPHAR\SebastianBergmann\Exporter.
Read more in the article: One version per process
Can I run my tests against the next PHP version before it is released?
Yes, and PHPUnit is what makes that possible. I test PHPUnit against a new PHP version as soon as its development begins. Work on PHP 8.6 started on 23 September 2025, and since 26 September 2025 every change pushed to any branch of PHPUnit has also been tested against PHP 8.6. Only when PHPUnit works with the development versions of PHP can other projects run their own test suites against them. The setup-php action by Shivam Mathur provides nightly builds of PHP for this. In the other direction, the PHP project tests Laravel, Symfony, WordPress and PHPUnit, among others, against its development version every night.
Read more in the article: How PHP and its ecosystem test each other
Can I bet a decade on PHP?
I consider the answers PHP gives to the questions an architecture decision is actually made on strong, but not unqualified. Nobody can relicense PHP. From PHP 8.6 onwards, it is distributed under version 4 of the PHP License, which is BSD-3-Clause with nothing else attached. Its copyright is spread across hundreds of contributors, and there is no entity to be acquired. A new version has been released every November for a decade. Each gets two years of active support and two years of security fixes, and upgrades are mostly mechanical. Against that stand two risks: a large share of the paid maintenance depends on a shrinking sponsor base, and only about 140 people actually vote on changes to the language.
Read more in the article: A language has non-functional requirements, too
Training and Consulting
Can you train my team on PHPUnit?
Yes. Every course is also available for your team alone, online or on site, from “Test Automation with PHPUnit” to “Advanced PHPUnit”. The training overview lists all courses and their next public dates.
Can you help make an existing, hard-to-test codebase testable?
That is a large part of what I do. Through consulting and coaching I help teams bring legacy code under test and build the habits that keep it that way. How that works is described under consulting and coaching.
Can you help us set up quality gates for working with coding agents?
Yes. Apart from PHPUnit, this is one of my two focus areas at the moment. When an LLM-based coding agent writes the code, tests that define what the code should do, static analysis and reviews decide what of it reaches your repository. How I approach this is described under consulting and coaching.
Can you help us with supply chain security?
Yes. It is my other focus area apart from PHPUnit. I help teams keep Composer dependencies current without trusting every update blindly, harden their GitHub Actions, and document what their software contains, as ISO/IEC 18974 and the Cyber Resilience Act require. More on this under consulting and coaching.