skip to content

PHPUnit

The PHP testing engine others build on: TestCase classes with assertions, attribute-driven data providers, mocks and stubs, and phpunit.xml with coverage. Interviewers probe its current API.

on this pageshow

explore

questions

20

In a PHPUnit 13 phpunit.xml, what do the <testsuites> element, the bootstrap attribute and the <source> element each configure?

level: juniorimportance: must knowfreq 50%

answer

  1. which tests, what runs first, which code is yours
  2. directory suffix defaults to Test.php
  3. bootstrap is included once, usually vendor/autoload.php
  4. no <source>, no coverage
  5. phpunit.xml before phpunit.dist.xml

basics

~10 s

<testsuites> names sets of test directories or files; bootstrap names a PHP script, usually vendor/autoload.php, included once before tests load; <source> declares your first-party code, which coverage and issue reporting are scoped to.

solid answer

~40 s

`<testsuites>` holds named `<testsuite>` elements listing `<directory>` and `<file>` entries (a directory picks up files ending in `Test.php` by default) plus `<exclude>` paths; `--testsuite unit` runs one of them. The root `bootstrap` attribute names a script PHPUnit includes once before loading tests - normally `vendor/autoload.php`, or a small file that requires it and sets up the environment; an exception there aborts the run with "Error in bootstrap script". `<source>` declares first-party code with `<include>` and `<exclude>` directories: it is the filter for code coverage (without it PHPUnit warns "No filter is configured, code coverage will not be processed") and it scopes deprecation, notice and warning reporting. PHPUnit looks for `phpunit.xml`, then `phpunit.dist.xml`, then `phpunit.xml.dist` in the working directory.

code

xml · 23 lines
xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         cacheDirectory=".phpunit.cache">
    <testsuites>
        <testsuite name="unit">
            <directory>tests/Unit</directory>
        </testsuite>
        <testsuite name="integration">
            <directory>tests/Integration</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory>src</directory>
        </include>
        <exclude>
            <directory>src/Legacy/Generated</directory>
        </exclude>
    </source>
</phpunit>

go deeper

for a junior

Recall the three roles: <testsuites> picks the tests, bootstrap loads the autoloader first, <source> marks your own code for coverage.

for a middle

Explain the Test.php suffix default, per-suite bootstrap, and why coverage is skipped with a warning when <source> is missing.

for a senior

Show how you structure suites and <source> for a legacy codebase so unit runs stay fast and coverage excludes generated or vendor code.

for a principal

Treat the configuration as a team contract: a committed dist file, local overrides, and suite boundaries that match how CI stages are split.

## The file and how PHPUnit finds it PHPUnit 13 reads its settings from an **XML configuration file**. Run without `-c`/`--configuration`, it looks in the current directory for `phpunit.xml`, then `phpunit.dist.xml`, then `phpunit.xml.dist`, and uses the first it finds. The usual convention is to commit a `.dist` file and let a developer override it locally with an untracked `phpunit.xml`. `vendor/bin/phpunit --generate-configuration` writes a starter file, and `--validate-configuration` checks one against the schema; a file that fails validation still runs, but PHPUnit emits a warning that results may not be as expected - a PHPUnit warning, so the run fails by default. Three parts of the file answer three questions: **which tests**, **what runs before them**, and **which code is yours**. ## `<testsuites>`: which tests A **test suite** is a named collection of test files. - `<directory>tests/Unit</directory>` collects every file under that path whose name ends in the `suffix` attribute, which defaults to `Test.php`; a `prefix` attribute exists too. - `<file>` adds a single file; `<exclude>` removes a path from the suite. - Several `<testsuite>` elements let you split fast unit tests from slow integration tests. On the command line `--testsuite unit` runs one, `--testsuite unit,integration` runs several, and `--exclude-testsuite` works the other way. The root `defaultTestSuite` attribute picks the suite used when none is named. ## The `bootstrap` attribute: what runs first `bootstrap="vendor/autoload.php"` tells PHPUnit to `include_once` that script before it loads any test class. Its job is to make classes loadable - Composer's autoloader - and optionally to prepare the process: environment variables, a default time zone, constants. - If the file is missing, PHPUnit stops with an error naming it. - If the script throws, PHPUnit aborts with "Error in bootstrap script" plus the exception and its trace. - A `<testsuite>` element can carry its own `bootstrap` attribute; that script is loaded only when the suite is selected, so integration-only setup does not slow unit runs. Keep the bootstrap small. Anything it does happens once per process and is shared by every test, so it is the wrong place for per-test fixtures. ## `<source>`: which code is yours `<source>` declares your **first-party code**: ```xml <source> <include><directory>src</directory></include> <exclude><directory>src/Legacy/Generated</directory></exclude> </source> ``` A `<directory>` here picks up files ending in `.php` by default. PHPUnit uses the declaration for two different jobs: 1. **Code coverage filter.** Only files in `<source>` appear in coverage reports. With no `<source>` at all PHPUnit emits a warning, "No filter is configured, code coverage will not be processed", and writes no report; if the paths match no files, it says so and again skips coverage. Uncovered files are listed at 0 % by default, because the coverage element's `includeUncoveredFiles` attribute defaults to true. 2. **Issue attribution.** Attributes such as `restrictNotices`, `restrictWarnings` and `ignoreIndirectDeprecations` use `<source>` to tell whether a notice or deprecation came from your code or from a dependency in `vendor/`. ## Putting it together | Element or attribute | Answers | Typical value | |---|---|---| | `<testsuites>` | Which test files run | `tests/Unit`, `tests/Integration` | | `bootstrap` | What is loaded before tests | `vendor/autoload.php` | | `<source>` | Which code is first-party | `src` | | `cacheDirectory` | Where run history and caches live | `.phpunit.cache` | ## A legacy project's first configuration When a legacy application arrives without any PHPUnit setup, the order of work matters. Start with one `unit` suite that only contains tests which need no database or network, a bootstrap that loads the autoloader and nothing else, and a `<source>` that points at the application code while excluding generated, vendored or template directories. Add the `integration` suite afterwards, with its own bootstrap for credentials and schema setup. This keeps the first CI job fast and green, and it makes every later step - stricter settings, coverage reports - a change to one well-understood file rather than a rewrite. ## Common mistakes 1. Leaving `<source>` out and wondering why `--coverage-html` produced nothing. 2. Naming test files `UserTests.php` or `user_test.php`: they do not end in `Test.php`, so the directory scan skips them silently. 3. Putting database seeding in the bootstrap, so it runs once for the whole process instead of once per test that needs it. 4. Pointing `<source>` at the project root, which drags `vendor/` and tests into coverage. 5. Keeping an old file whose coverage paths still live under the pre-`<source>` layout; `--migrate-configuration` rewrites older formats.

  • In PHPUnit 13, why is a file named InvoiceTests.php under tests/Unit never run?
    A `<directory>` inside `<testsuite>` only collects files ending in its `suffix`, which defaults to `Test.php`. `InvoiceTests.php` ends in `Tests.php`, so the scan skips it without any message. Rename the file, or add a `<file>` entry or a `suffix` that matches the project's convention.
  • With PHPUnit 13, when would you give a single <testsuite> its own bootstrap attribute?
    When one suite needs setup the others do not - for example integration tests that load database credentials or start a schema migration. The per-suite script is loaded only when that suite is selected, so `--testsuite unit` stays fast and free of integration dependencies. The root bootstrap still runs first.

saying these in an interview costs you the question

  • Thinks the bootstrap script runs before every test method
  • Believes coverage works without a <source> element
  • Assumes any PHP file under the test directory is collected
  • Points <source> at the project root including vendor/
  • Confuses <source> with the list of test directories
open as a page

With a PHPUnit 13 test stub, how do you make a method return a value, choose one by argument, compute one, or throw?

level: juniorimportance: must knowfreq 56%

basics

~10 s

Name the method with method(), then attach an answer: willReturn() for fixed values, willReturnMap() to pick by arguments, willReturnCallback() to compute from them, and willThrowException() to throw.

open as a page

With PHPUnit, what is the difference between assertSame and assertEquals, and when does choosing the wrong one hide a bug?

level: juniorimportance: must knowfreq 75%

basics

~20 s

assertSame checks identity with ===: same type and value, and for objects the same instance. assertEquals checks loose equality: 90 equals '90', and separate objects with equal properties are equal. Loose checks can pass when the type is wrong.

open as a page

In PHPUnit 13, how does a #[DataProvider] method feed a test, and why must that provider be public and static?

level: middleimportance: must knowfreq 60%

basics

~20 s

#[DataProvider('name')] points a test at a public static method returning an iterable of argument arrays. PHPUnit calls it while building the suite, before any setUp(), so there is no instance to call it on; each array becomes one test run.

open as a page

In PHPUnit 13, what is the difference between createStub() and createMock(), and how do you choose between them?

level: middleimportance: must knowfreq 64%

basics

~20 s

createStub() returns a Stub that only answers calls with configured values; createMock() returns a MockObject that can also carry expects() rules that PHPUnit enforces. Stub collaborators that feed data in, mock those whose calls are the outcome.

open as a page

In PHPUnit 13, how do you assert that code throws an exception, and what are the traps of expectException?

level: middleimportance: must knowfreq 60%

basics

~10 s

Call expectException(SomeException::class) before the code that should throw. The test passes only if a matching Throwable escapes; nothing after the throwing line runs. Message checks use expectExceptionMessageIs, expectExceptionMessageIsOrContains or expectExceptionMessageMatches.

open as a page

With PHPUnit 13, how do you run one test method against several inline inputs using the #[TestWith] attribute?

level: juniorimportance: should knowfreq 34%

basics

~10 s

Stack one #[TestWith([...])] attribute per case on the test method. Each array is unpacked into the method's parameters, and PHPUnit runs the method once per attribute, reporting every case as its own test.

open as a page

With PHPUnit, what do assertCount and assertInstanceOf check, and why prefer them over assertTrue with count() or instanceof?

level: juniorimportance: should knowfreq 35%

basics

~10 s

assertCount checks the number of elements in a Countable or iterable, and assertInstanceOf checks a value's class or interface. Both report expected against actual on failure, where assertTrue only says false is not true.

open as a page

In PHPUnit 13, how does a TestCase subclass declare its tests, and when do setUp and tearDown run?

level: juniorimportance: should knowfreq 58%

basics

~20 s

A test class extends PHPUnit\Framework\TestCase; each public method whose name starts with test, or that carries #[Test], is a test. PHPUnit creates a fresh instance per test, calling setUp before and tearDown after each test.

open as a page

With PHPUnit 13, how do --filter and --testsuite narrow a test run, and what pattern syntax does --filter accept?

level: middleimportance: should knowfreq 42%

basics

~10 s

--testsuite selects named suites from phpunit.xml; --filter selects individual tests by a case-insensitive regular expression matched against Class::method plus any data-set name, with shortcuts such as #2 or @name for data sets.

open as a page

With PHPUnit 13, what makes a test risky, and why does a run with risky tests still exit with code 0 unless failOnRisky is set?

level: middleimportance: should knowfreq 40%

basics

~20 s

A risky test passed but broke a rule PHPUnit checks, such as performing no assertions or printing output. Risky results are reported but do not fail the run by default, because failOnRisky defaults to false.

open as a page

In PHPUnit 13, how do expects($this->once()) and with() check a mocked mailer's call, and when does a mismatch fail the test?

level: middleimportance: should knowfreq 50%

basics

~20 s

expects($this->once()) sets how many calls must happen and with() constrains each call's arguments. A wrong argument or an extra call fails at the moment of the call; a missing call fails when PHPUnit verifies mocks after the test method.

open as a page

In CI, how do you make PHPUnit 13 produce HTML and Clover coverage reports, and how do you choose between PCOV and Xdebug as the driver?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Install a coverage driver - PCOV for fast line coverage, Xdebug in coverage mode for branch and path coverage - declare <source>, then pass --coverage-html and --coverage-clover or configure <coverage><report>. Without a driver PHPUnit warns and writes nothing.

open as a page

A PHPUnit 13 suite passes in default order but fails intermittently with executionOrder="random"; how do you reproduce the failure and find its cause?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Random order exposes tests that depend on state another test left behind. Take the Random Seed PHPUnit prints, replay it with --random-order-seed, find the test that runs earlier and leaks state, and make each test set up its own.

open as a page

With PHPUnit 13, what do #[CoversClass] and #[UsesClass] declare on a test class, and how do they change the coverage a test contributes?

level: seniorimportance: should knowfreq 30%

basics

~10 s

#[CoversClass(X::class)] declares which code a test class intends to test; #[UsesClass(Y::class)] declares code it may execute without testing. With coverage on, only lines in covered targets are credited to that test.

open as a page

After upgrading to PHPUnit 13, a legacy suite still uses @dataProvider, @depends, @group and @covers docblocks; what breaks, and how do you migrate?

level: seniorimportance: should knowfreq 42%

basics

~20 s

PHPUnit 13 reads only attributes, so the docblock tags are ignored without any error. Parameterised tests fail with ArgumentCountError, while dropped groups and coverage targets fail quietly; migrate each tag to its attribute and import it.

open as a page

After an upgrade to PHPUnit 13, a green suite reports notices and deprecations from its mocks; what do they mean and how do you clear them?

level: seniorimportance: should knowfreq 32%

basics

~10 s

They flag mocks that verify nothing: a mock with no expectations, expects($this->any()), or with() without expects(). Replace such mocks with createStub(), state a real count rule, or opt out with #[AllowMockObjectsWithoutExpectations].

open as a page

When upgrading an older suite to PHPUnit 13, how do you replace mock expectations written with withConsecutive() or at()?

level: seniorimportance: should knowfreq 42%

basics

~10 s

Both are gone. Use expects($this->exactly(n)) with withParameterSetsInOrder() to check ordered arguments, willReturn($a, $b) for per-call returns, or willReturnCallback() when the answer depends on the arguments.

open as a page

A PHPUnit test passes when run alone but fails when its whole class runs; how can setUpBeforeClass or static properties cause that?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Instance properties are rebuilt for every test, but static properties and fixtures created in setUpBeforeClass are shared by all tests in the class. An earlier test that mutates them changes what a later test sees.

open as a page

With PHPUnit 13, how does #[Depends] pass a value between test methods, and what happens when the producer test fails?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

#[Depends('testProducer')] makes a test receive the producer's return value as an argument and run only if the producer passed in the same run. A failed, skipped or unrun producer makes PHPUnit skip the dependent test.

open as a page