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
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
my-app/
├── composer.json
├── phpunit.xml
├── src/
│ └── Billing/
│ └── InvoiceCalculator.php
└── tests/
└── Billing/
└── InvoiceCalculatorTest.phpA 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
namespace App\Billing;
class InvoiceCalculator
{
public function total(float $subtotal, float $taxRate): float
{
return round($subtotal * (1 + $taxRate), 2);
}
}tests/Billing/InvoiceCalculatorTest.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/
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 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/phpunitadds PHPUnit as a dev dependency and createsvendor/bin/phpunit.Structure: put tests in
tests/, mirroring the folder layout ofsrc/, with each test class named after the class it covers plusTest.Write: extend
PHPUnit\Framework\TestCase; eachpublic function testSomething()method is one test.Run:
vendor/bin/phpunit(with aphpunit.xmlin place) runs the whole configured suite.