0

PHPUnit setup, phpunit.xml, test autoloading

Beginner5 min read·php-14-001

Concept

PHPUnit is the de facto testing framework for PHP, and getting its configuration right is as important as writing the tests themselves. The entry point is phpunit.xml (or phpunit.xml.dist for version-controlled defaults), which tells PHPUnit where your test files live, how to bootstrap autoloading, and which coverage drivers to use.

The testsuites element groups tests into logical collections—typically "Unit" pointing at tests/Unit/ and "Feature" pointing at tests/Feature/. Each suite can specify multiple directory or file entries. PHPUnit discovers test classes by scanning for files matching the configured pattern (default *Test.php) and then finding methods prefixed with test or annotated with #[Test].

Autoloading is handled through Composer. Your composer.json should define an autoload-dev section mapping the Tests\ namespace to tests/. After running composer dump-autoload, PHPUnit's bootstrap file (typically vendor/autoload.php) handles the rest. Never hardcode require paths inside test files.

A critical but often overlooked setting is colors="true" and stopOnFailure. In CI you usually want colors="false" and you may want stopOnDefect to halt on risky tests. The <php> element lets you set INI values and environment variables scoped to the test run—set APP_ENV to testing here, not in your .env.testing, to avoid confusion.

SettingRecommended ValueWhy
cacheDirectory.phpunit.cache/Speeds up coverage runs
processIsolationfalseDefault; use per-test #[RunInSeparateProcess] when needed
stopOnDefecttrue in devCatch risky tests immediately
bootstrapvendor/autoload.phpLet Composer handle autoloading

Code Example

php
<?php
// phpunit.xml (project root)
/*
<?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"
    colors="true"
    cacheDirectory=".phpunit.cache"
    stopOnDefect="false"
>
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">tests/Unit</directory>
        </testsuite>
        <testsuite name="Feature">
            <directory suffix="Test.php">tests/Feature</directory>
        </testsuite>
    </testsuites>

    <source>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <exclude>
            <directory>src/Legacy</directory>
        </exclude>
    </source>

    <coverage>
        <report>
            <html outputDirectory="coverage-html"/>
            <clover outputFile="coverage.xml"/>
        </report>
    </coverage>

    <php>
        <ini name="display_errors" value="1"/>
        <env name="APP_ENV" value="testing"/>
        <env name="DB_CONNECTION" value="sqlite"/>
        <env name="DB_DATABASE" value=":memory:"/>
    </php>
</phpunit>
*/

// composer.json autoload-dev section:
/*
"autoload-dev": {
    "psr-4": {
        "Tests\\": "tests/"
    }
}
*/

// tests/TestCase.php — your base test class
declare(strict_types=1);

namespace Tests;

use PHPUnit\Framework\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        // shared test bootstrap logic
    }
}

Interview Q&A

Q: What is the difference between phpunit.xml and phpunit.xml.dist, and which should be committed to version control?

phpunit.xml.dist is the distributable default configuration that ships with your project in version control. phpunit.xml is a developer-local override that should be listed in .gitignore. PHPUnit loads phpunit.xml if it exists, falling back to phpunit.xml.dist. This pattern lets individual developers override settings (like enabling stopOnFailure locally) without affecting CI or other team members.


Q: Why should you never call require or include inside test files, and what is the correct alternative?

Hardcoded require statements create brittle paths that break as the project structure changes and make it impossible to run a single test file in isolation. The correct approach is to configure Composer's autoload-dev with a PSR-4 mapping for the test namespace and use vendor/autoload.php as PHPUnit's bootstrap. This means any class in your tests/ directory is automatically available without explicit require statements.


Q: How do you configure PHPUnit to use an in-memory SQLite database for tests, and why is this preferable to a separate test database?

Set DB_CONNECTION=sqlite and DB_DATABASE=:memory: in the <php> section of phpunit.xml. An in-memory SQLite database is created fresh for each process, requires no cleanup between test runs, executes significantly faster than a network database, and eliminates the need for test database provisioning in CI. The trade-off is that SQLite's SQL dialect is not identical to MySQL or PostgreSQL, so schema-level tests (specific data types, JSON operators) may still need a real database.