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. Collect coverage separately for each run, for example when you shard or parallelise your suite, export it in PHP or Cobertura format, and merge the reports afterwards. This gives you one combined figure without forcing every test into a single run.

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 expose confidence that coverage alone cannot.

Read more in the article: Path Coverage or Mutation Testing?

Test Doubles (Mocks and Stubs)

What should I use instead of withConsecutive()?

withConsecutive() was removed in PHPUnit 10. Replace it with a single willReturnCallback() that inspects the arguments, combined where needed with the invocation matcher returned by $this->exactly() so you can react to each call by its number. It is more verbose but far clearer about what each invocation expects.

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

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

Speed and Performance

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

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

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

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

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

Static Analysis and Coding Standards

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

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. What used to be framed purely as a productivity problem is now part of the security conversation.

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?

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

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

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

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

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

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

Training and Consulting

Can Sebastian Bergmann train my team on PHPUnit?

Yes. I offer tailored workshops and training on PHPUnit, test automation, and testable software design, delivered remotely or on site. See the training section for the current topics.

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. The consulting section describes how that works.