PHPPHPUnit

PHPUnit

PHPUnit is the de facto standard testing framework for PHP. Nearly every major framework — Laravel, Symfony, WordPress plugin scaffolds — either ships with it configured or documents it as the default choice, and most PHP job postings and open-source contribution guides assume you already know your way around it. This page covers getting it installed, the directory convention almost every project follows, writing a first test, and running the suite from the command line.

Installing PHPUnit with Composer

PHPUnit is installed as a dev dependency through Composer, PHP's package manager. A dev dependency is code your project needs while you're building and testing it, but not in production — Composer keeps it out of production installs when you run composer install --no-dev.

Add PHPUnit to a project

Bash
composer require --dev phpunit/phpunit

This adds phpunit/phpunit under require-dev in composer.json and drops an executable at vendor/bin/phpunit. It's worth checking which major version got installed — PHPUnit's version numbers track supported PHP versions closely, and a project running PHP 8.1 will typically land on PHPUnit 10 or 11 rather than an older release built for PHP 7.

The tests/ directory convention

PHPUnit doesn't require a specific folder name, but the overwhelming convention — and the one every starter template and framework assumes — is a top-level tests/ directory that mirrors the structure of src/. If your application code lives at src/Billing/InvoiceCalculator.php, its test typically lives at tests/Billing/InvoiceCalculatorTest.php. Keeping the folder structure parallel makes it trivial to find the test for a given class, and vice versa.

A typical project layout

Bash
my-app/
├── composer.json
├── phpunit.xml
├── src/
│   └── Billing/
│       └── InvoiceCalculator.php
└── tests/
    └── Billing/
        └── InvoiceCalculatorTest.php
A minimal test case

A PHPUnit test is a class that extends PHPUnit\Framework\TestCase. Each public method whose name starts with test (or is annotated with the #[Test] attribute on newer PHPUnit versions) is treated as an individual test.

src/Billing/InvoiceCalculator.php

PHP
<?php

namespace App\Billing;

class InvoiceCalculator
{
    public function total(float $subtotal, float $taxRate): float
    {
        return round($subtotal * (1 + $taxRate), 2);
    }
}

tests/Billing/InvoiceCalculatorTest.php

PHP
<?php

namespace Tests\Billing;

use App\Billing\InvoiceCalculator;
use PHPUnit\Framework\TestCase;

class InvoiceCalculatorTest extends TestCase
{
    public function testTotalAddsTax(): void
    {
        $calculator = new InvoiceCalculator();

        $result = $calculator->total(100.0, 0.08);

        $this->assertSame(108.0, $result);
    }
}

Every test method follows the same shape: create the thing you're testing, call the method under test, then assert on the result with one of PHPUnit's assert* methods. $this->assertSame() checks both value and type, which for a float like this is exactly what you want.

Running the suite

With PHPUnit installed via Composer, the executable lives at vendor/bin/phpunit. Point it at your tests/ directory (or rely on phpunit.xml, covered next, to know where to look).

Run every test in tests/

Bash
vendor/bin/phpunit tests
PHPUnit 10.5.0 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 00:00.012, Memory: 6.00 MB

OK (1 test, 1 assertion)

Each dot on the progress line represents one passing test; an F means a failed assertion, and an E means the test threw an unexpected error or exception. On a large suite this line is often the first thing you glance at — a wall of dots with one F in the middle tells you exactly where to look before you've even read the summary.

Configuring phpunit.xml

Typing out the tests path (and any other options) on every run gets old fast. A phpunit.xml file in the project root lets you configure PHPUnit once and just run vendor/bin/phpunit with no arguments.

phpunit.xml

XML
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

The bootstrap attribute points at Composer's autoloader so PHPUnit can resolve your namespaced classes without a manual require. The testsuites block tells it where to find tests; a larger project might split this into multiple named suites — Unit, Integration — so you can run just one at a time with vendor/bin/phpunit --testsuite=Unit.

  • Install: composer require --dev phpunit/phpunit adds PHPUnit as a dev dependency and creates vendor/bin/phpunit.

  • Structure: put tests in tests/, mirroring the folder layout of src/, with each test class named after the class it covers plus Test.

  • Write: extend PHPUnit\Framework\TestCase; each public function testSomething() method is one test.

  • Run: vendor/bin/phpunit (with a phpunit.xml in place) runs the whole configured suite.

Commit phpunit.xml, not phpunit.xml.dist alone
Older PHPUnit tutorials mention a `phpunit.xml.dist` file meant to be copied to a git-ignored `phpunit.xml`. Modern projects almost always just commit `phpunit.xml` directly — there's rarely a need for machine-specific overrides, and committing it means a fresh clone can run the suite immediately with no setup step.
Tip
Add a `test` script to `composer.json` — `"test": "phpunit"` under `scripts` — so the whole team (and your CI configuration) can run `composer test` instead of remembering the exact `vendor/bin/phpunit` path and flags.