Home page: open_in_new https://phpunit.de .
Installation
composer require --dev phpunit/phpunit
Files
Two files are commonly involved when running tests:
bootstrap.php— a file run before the test suite starts. It’s typically used for autoloading classes and other setup, so you don’t needrequire_oncefor every file that needs to be loaded.phpunit.xml— the XML configuration file for the test suite.
Writing tests and assertions
A test is a PHP class that extends PHPUnit\Framework\TestCase (the modern, namespaced class; older code and tutorials may still refer to PHPUnit_Framework_TestCase).
use PHPUnit\Framework\TestCase;
function sum($a, $b)
{
return $a + $b;
}
class MathTest extends TestCase
{
public function testSum()
{
$this->assertEquals(2, sum(1, 1));
}
}
Common assertions:
assertTrue()— verifies that a condition is true.assertFalse()— verifies that a condition is false.assertEquals()— verifies that expected and actual values are equal, similar to the==operator.assertSame()— similar toassertEquals(), but checks that values are identical, similar to the===operator.assertNull()— verifies that a value isnull.assertEmpty()— verifies that a value is empty, using the same rules as PHP’sempty()(false,null,'',[], and so on).
open_in_new Full list of assertions .
If the built-in assertions aren’t enough, you can write a custom one by extending PHPUnit\Framework\Constraint\Constraint.
Command-line usage
Get the installed version:
phpunit --version
Run a single test file:
phpunit UserTest.php
Run every test in a directory by passing the directory as the first argument:
phpunit .
Output symbols:
.— the test passed.F— the test failed, or an assertion didn’t match.E— an error was triggered during test execution.S— the test was skipped.I— the test is incomplete or not implemented yet.
Skip a test from inside the test method:
$this->markTestSkipped('We don\'t want to test this now');
Mark a test as not implemented yet:
$this->markTestIncomplete('Needs to be implemented');
This matters because an empty test method would otherwise be reported as passing.
Logging and coverage
--log-junit— stores results in the Java JUnit XML format, which many CI tools understand.--log-json— stores results as JSON. Easy to process, but has no fixed schema to validate against.--coverage-clover— generates code coverage in the Clover XML format.--coverage-html— generates a browsable HTML coverage report.--coverage-php— generates a serializedPHP_CodeCoverageobject for programmatic use.--coverage-text— generates a plain-text coverage report.
Selecting which tests to run
--filter— runs only tests matching a pattern, e.g.phpunit --filter Validation UserTest.php.--testsuite— like--filter, but matches against test suite names.--group— runs only tests annotated with a given@group:
/**
* @group math
*/
public function testOnePlusOne()
--exclude-group— excludes a group from execution.--list-groups— lists all available groups.--test-suffix— sets the test file suffix (Test.phpby default).
Stopping early
--stop-on-error— stops on the first recoverable error (not a fatal error).--stop-on-failure— stops on the first failed assertion.--stop-on-skipped— stops when any test is marked as skipped.--stop-on-incomplete— stops when any test is marked as incomplete.
Other useful flags
--strict— strict mode (unrelated to PHP’sE_STRICT).--verbose— more detailed output, e.g. the line where a test failed.--debug— extra debugging information during execution.--process-isolation— runs each test in its own process; helps with memory issues but is much slower.--no-globals-backup— disables backup/restore of$GLOBALSbefore each test.--static-backup— backs up and restores static class properties before each test.--bootstrap— sets the bootstrap file, usually the one with the class loader.--configuration— sets the XML configuration file (usuallyphpunit.xml).--no-configuration— ignores the defaultphpunit.xml.--include-path— prepends extra include paths for your PHP files.-d key=value— overrides aphp.inivalue for the run.
The phpunit.xml configuration file
phpunit.xml is the default configuration filename. PHPUnit looks for it automatically when run from the command line. To use another filename:
phpunit --configuration config.xml
Basic structure:
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit backupGlobals="true" backupStaticAttributes="false"
strict="false" verbose="true">
<php>
<includePath>./Pear</includePath>
<ini name="memory_limit" value="128M"/>
<env name="ENVIRONMENT" value="test"/>
<post name="is_test" value="1"/>
</php>
</phpunit>
Attributes and their command-line equivalents:
backupGlobals—--no-globals-backup.backupStaticAttributes—--static-backup.strict—--strict.verbose—--verbose.
The <php> block lets you prepare the test environment:
<includePath>callsset_include_path().<ini>callsini_set()for a PHP configuration value.<env>sets an environment variable.<post>sets a$_POSTvalue (here,$_POST['is_test'] = 1).
The same applies to $_GET, $_COOKIE, and any other global except $_SESSION.
Test listeners
Listeners let you hook into the test execution process — for example, to record results into a database. A listener implements PHPUnit\Framework\TestListener:
interface TestListener
{
public function addError(Test $test, \Throwable $t, float $time): void;
public function addFailure(Test $test, AssertionFailedError $e, float $time): void;
public function addIncompleteTest(Test $test, \Throwable $t, float $time): void;
public function addSkippedTest(Test $test, \Throwable $t, float $time): void;
public function startTestSuite(TestSuite $suite): void;
public function endTestSuite(TestSuite $suite): void;
public function startTest(Test $test): void;
public function endTest(Test $test, float $time): void;
}
Each method fires on a specific event, such as startTest or startTestSuite. Register a listener in phpunit.xml:
<listeners>
<listener class="SomeListener">
<arguments>
<string>For listener constructor</string>
</arguments>
</listener>
</listeners>
Code coverage filtering
Use the <filter> tag to control what’s included in coverage reports — blacklist for exclusions, whitelist for inclusions:
<filter>
<blacklist>
<directory suffix=".php">vendor/zendframework</directory>
<directory suffix=".php">vendor/symfony</directory>
</blacklist>
<whitelist>
<directory suffix=".php">src</directory>
</whitelist>
</filter>
Organizing tests: suites and groups
There are two ways to organize which tests run, using the configuration file:
- Test suites.
- Groups included or excluded from a run.
One common reason to split tests into sets is to separate fast unit tests from slower integration tests (which may need a real database, for instance). You might want to run integration tests separately, e.g. as a post-commit hook, so a developer is notified quickly if they pushed bad code, without waiting on the slow suite.
Test suites group tests into named batches, each including or excluding specific files/directories:
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
<testsuites>
<testsuite name="Permissions">
<directory>./library/Permissions</directory>
<exclude>./library/Permissions/ManagerTest.php</exclude>
</testsuite>
<testsuite name="Util">
<file>./library/Util/PasswordTest.php</file>
</testsuite>
</testsuites>
</phpunit>
Here, the Permissions suite runs every test under ./library/Permissions except ManagerTest.php, and Util runs only PasswordTest.php. You could create a separate configuration file for the remaining tests (e.g. DB and FileTest.php):
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
<testsuites>
<testsuite name="DB">
<directory>./library/DB</directory>
</testsuite>
<testsuite name="Util">
<file>./library/Util/FileTest.php</file>
</testsuite>
</testsuites>
</phpunit>
Groups are assigned via annotations:
/**
* @group security
*/
public function testCheckPassword() {}
/**
* @group security
* @group user
*/
public function testUpdatePassword() {}
List all groups with phpunit --list-groups, and include/exclude them from the configuration file:
<?xml version="1.0" encoding="UTF-8" ?>
<phpunit>
<groups>
<include>
<group>security</group>
</include>
<exclude>
<group>user</group>
</exclude>
</groups>
</phpunit>
Bootstrap file
A bootstrap file (usually bootstrap.php) runs before the tests and sets up things like the include path, environment variables, and the class loader. You could set some of the same things via phpunit.xml, but doing it in PHP code is cleaner and works the same way your application’s own bootstrapping does:
<?php
ini_set('memory_limit', '512M');
error_reporting(E_ALL | E_STRICT);
defined('APPLICATION_ENV') || define('APPLICATION_ENV',
getenv('APPLICATION_ENV') ?: 'development');
spl_autoload_register('loadClass');
function loadClass($className)
{
$base_dir = __DIR__ . '/src/';
$test_dir = __DIR__ . '/tests/';
set_include_path($base_dir . PATH_SEPARATOR . $test_dir);
$file = str_replace('\\', '/', $className) . '.php';
if (file_exists($file)) {
require_once $file;
}
}
Test fixtures
To set up a known state for tests, you have two scopes:
- Per test method — runs before/after each test.
- Per test case class — runs once before/after the whole class.
Per test method: setUp() and tearDown()
setUp() runs before every test, so shared setup code doesn’t need to be duplicated across similar tests:
class CounterTest extends TestCase
{
private static $countFrom = 1;
private $result = 0;
public function setUp(): void
{
$this->result = self::$countFrom;
}
public function testAdd()
{
$this->result += 1;
$this->assertEquals(2, $this->result);
}
public function testTakeAway()
{
$this->result -= 1;
$this->assertEquals(0, $this->result);
}
}
tearDown() is the opposite of setUp() and runs after each test — useful for cleanup like closing an open file pointer.
Per test case class: setUpBeforeClass() and tearDownAfterClass()
These run once when the test case class is loaded, not before/after every test — handy for something expensive to set up once, like a single shared database connection:
class CounterTest extends TestCase
{
private static $countFrom = 1;
private $result = 0;
public static function setUpBeforeClass(): void
{
self::$countFrom = 2;
}
public function setUp(): void
{
$this->result = self::$countFrom;
}
public function testAdd()
{
$this->result += 1;
$this->assertEquals(3, $this->result);
}
public function testTakeAway()
{
$this->result -= 1;
$this->assertEquals(1, $this->result);
}
}
Data providers
A data provider feeds one or more input values into a test method — hardcoded or loaded dynamically (e.g. from a database in an integration test). Reference the provider method with the @dataProvider annotation; it must return an array of arrays (or an Iterator), one inner array per test run:
class DataProvidersTest extends TestCase
{
/**
* @return array
*/
public function providerArray()
{
return [
'small' => [[1, 2, 3, 4, 5]],
'medium' => [[10, 20, 30, 40, 50]],
'large' => [[100, 200, 300, 400, 500]],
];
}
/**
* @dataProvider providerArray
*/
public function testPop(array $inputArray)
{
$expectedResult = count($inputArray) - 1;
array_pop($inputArray);
$this->assertEquals($expectedResult, count($inputArray));
}
/**
* @dataProvider providerArray
*/
public function testSum(array $inputArray)
{
$this->assertEquals(array_sum($inputArray), array_sum($inputArray));
}
}
Each test method above actually runs three times — once per element returned by the data provider.
Test dependencies
Test dependencies are useful for splitting a complex test into several smaller ones. The @depends annotation says one test depends on another; the producer test can return a value that PHPUnit passes as an argument to the consumer test:
class DependsTest extends TestCase
{
public function testArrayFill()
{
$testedArray = array_fill(0, 11, 1);
$this->assertIsArray($testedArray);
return $testedArray;
}
/**
* @depends testArrayFill
*/
public function testPop(array $inputArray)
{
array_pop($inputArray);
$this->assertEquals(10, count($inputArray));
return $inputArray;
}
/**
* @depends testPop
*/
public function testSum(array $inputArray)
{
$this->assertEquals(10, array_sum($inputArray));
}
}
PHPUnit doesn’t guarantee test execution order in general, but here testPop depends on testArrayFill and testSum depends on testPop. So testArrayFill runs first, its returned array is passed to testPop, which removes one element and passes the result to testSum. This lets you control run order and share data between tests.
Testing exceptions
You can catch the exception manually:
public function testCreateUserException()
{
$db = new \PDO('mysql:host=localhost;port=3306;dbname=test', 'root');
$config = new \stdClass();
$config->email = 'test@example.com';
$config->site_url = 'http://example.com';
$email = $this->createMock(\Util\Mail::class);
$userManager = new UserManager($email, $db, $config);
$user = new User([
'firstName' => 'FirstName',
'lastName' => 'LastName',
'email' => null,
'password' => 'password123',
]);
try {
$userManager->createUser($user);
$this->fail();
} catch (\InvalidArgumentException $e) {
// correct behavior
}
}
A cleaner way is to declare the expected exception before calling the code under test:
public function testCreateUserException()
{
$this->expectException(\InvalidArgumentException::class);
$db = new \PDO('mysql:host=localhost;port=3306;dbname=test', 'root', '');
$config = new \stdClass();
$config->email = 'test@example.com';
$config->site_url = 'http://example.com';
$user = new User([
'firstName' => 'FirstName',
'lastName' => 'LastName',
'email' => null,
'password' => 'password123',
]);
$email = $this->createMock(\Util\Mail::class);
$userManager = new UserManager($email, $db, $config);
$userManager->createUser($user);
}
expectException() is the modern replacement for the old @expectedException annotation and the deprecated setExpectedException() method.
Mocking with createMock()
$email = $this->createMock(\Util\Mail::class);
This gives you an object with every method of Mail, but with no real implementation — calls simply return null. That’s exactly what you want in a test: the code path executes, but no real e-mail gets sent.
getMock()was the method used in older PHPUnit versions; it was removed, andcreateMock()is the current equivalent.
Global state
Backing up and restoring global state matters most when testing legacy code. PHPUnit can back up and restore $GLOBALS, $_ENV, $_POST, $_GET, $_COOKIE, $_SERVER, $_FILES, and $_REQUEST around each test method.
Relying on global state is generally discouraged — the whole point of unit testing is isolation, and tests should stay fast, independent, and reliable. Still, it can be the only practical way to set up a known environment when testing legacy code. Typical problems it causes:
- Unwanted side effects — one piece of code changes global state that another piece of code wasn’t expecting to change.
- Hidden dependencies — a required dependency becomes hard to spot, which is risky when changing code.
- Unclear responsibility — hard to reason about behavior that changes depending on hidden global state.
Only serializable state can be backed up — resources like a database connection or a file pointer can’t be.
Toggling the backup
- Command-line: a dedicated switch.
phpunit.xml: a configuration attribute.- Test case level:
protected $backupGlobals = true;
- Test level, via annotation:
/**
* @backupGlobals disabled
*/
Exclude specific globals from the backup/restore:
protected $backupGlobalsBlacklist = ['variable'];
Backing up static properties
protected $backupStaticAttributes = true;
Or via annotation:
/**
* @backupStaticAttributes disabled
*/
Exclude specific static properties:
protected $backupStaticAttributesBlacklist = ['propertyA'];
These options only take effect when set at the class level (as a property override or class-level annotation). Setting them inside
setUp()has no effect.
Example
/**
* @backupGlobals enabled
*/
class GlobalTest extends TestCase
{
public static function setUpBeforeClass(): void
{
$config = new \stdClass();
$config->date = new \DateTime('today');
$GLOBALS['config'] = $config;
}
public function testToday()
{
$today = new \DateTime('today');
$this->assertTrue($today == $GLOBALS['config']->date);
// Not affecting global state — backupGlobals is enabled
$GLOBALS['config']->date = new \DateTime('tomorrow');
}
/**
* @backupGlobals disabled
*/
public function testTomorrow()
{
$tomorrow = new \DateTime('tomorrow');
$this->assertTrue($tomorrow > $GLOBALS['config']->date);
// Will change the global value
$GLOBALS['config']->date = $tomorrow;
}
public function testTomorrowIsolated()
{
$tomorrow = new \DateTime('tomorrow');
$this->assertEquals($tomorrow, $GLOBALS['config']->date);
}
}
When backupGlobals is disabled for a test, changes to $GLOBALS['config'] persist into later tests (here, to “tomorrow”); otherwise the value is restored and stays at today’s date.
Requirements
If a test doesn’t satisfy its declared requirements, PHPUnit marks it as skipped instead of running it:
- PHP — minimum required PHP version, e.g.
@requires PHP 8.1. - PHPUnit — minimum required PHPUnit version, e.g.
@requires PHPUnit 10.0. - function — uses
function_exists()to check that a function is available (in your code or a loaded extension), e.g.@requires function Imagick::readImage. - extension — the required PHP extension, e.g.
@requires extension pdo_sqlite.
What makes a good test?
Generic rules that apply to any unit test, not just PHPUnit:
- Independent — each test should run independently of other tests and of the environment.
- Fast — tests need to be fast so you can (and will) run them often, e.g. as pre-/post-commit hooks.
- Repeatable — running a test many times should always give the same result.
- Up to date — code changes over time; if tests aren’t kept up to date, the investment in writing them is wasted. Whoever breaks a test should be the one who fixes it.
- Short — a test should be a few lines, easy to read and understand at a glance.
- Resilient — once written, a test shouldn’t need to change until the tested behavior itself changes.
When to write tests
One approach is test-driven development: design your class as an interface with no implementation yet, write tests describing exactly what you expect from each method, then implement the class so the tests pass.
Another approach is to write the class or function first, then write tests to verify it works as expected. Either way, write the test the same day you write the code — later you’ll have moved on to something else and it likely won’t happen.
Core functionality should always be covered by unit tests — this isn’t optional.
Andrew Dorokhov