PHPUnit Basics
PHPUnit is the most popular testing framework for PHP applications. It provides a comprehensive set of tools for writing and running unit tests, making it the industry standard for PHP testing. Whether you're building a simple script or a large Laravel application, PHPUnit is your go-to testing tool.
What is PHPUnit?
PHPUnit is a programmer-oriented testing framework for PHP created by Sebastian Bergmann. It's a member of the xUnit family of testing frameworks and provides:
- A rich set of assertions for verifying code behavior
- Test organization and grouping capabilities
- Test fixtures for setup and teardown
- Code coverage analysis
- Mock objects for isolating tests
- Data providers for testing multiple scenarios
Industry Standard: PHPUnit is used by major PHP projects including Laravel, Symfony, WordPress, Drupal, and thousands of other projects worldwide.
Installation
PHPUnit can be installed via Composer, the PHP dependency manager:
# Install PHPUnit as a development dependency
composer require --dev phpunit/phpunit
# Verify installation
./vendor/bin/phpunit --version
For Laravel projects, PHPUnit comes pre-installed:
# Laravel already includes PHPUnit
php artisan test
# Or use PHPUnit directly
./vendor/bin/phpunit
Configuration
PHPUnit uses an XML configuration file (usually phpunit.xml) to define test settings:
<?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">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory>tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./app</directory>
</include>
</coverage>
</phpunit>
Writing Your First Test
PHPUnit tests are classes that extend PHPUnit\Framework\TestCase:
<?php
// tests/Unit/CalculatorTest.php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function test_addition()
{
// Arrange
$calculator = new Calculator();
// Act
$result = $calculator->add(2, 3);
// Assert
$this->assertEquals(5, $result);
}
public function test_subtraction()
{
$calculator = new Calculator();
$result = $calculator->subtract(10, 4);
$this->assertEquals(6, $result);
}
}
?>
Naming Convention: Test methods must start with test or use the @test annotation. Test class names should end with Test.
Running Tests
Execute tests using the PHPUnit command:
# Run all tests
./vendor/bin/phpunit
# Run specific test file
./vendor/bin/phpunit tests/Unit/CalculatorTest.php
# Run specific test method
./vendor/bin/phpunit --filter test_addition
# Run with coverage report
./vendor/bin/phpunit --coverage-html coverage
# Run tests in a specific suite
./vendor/bin/phpunit --testsuite Unit
Assertions
Assertions are the heart of PHPUnit. They verify that your code behaves correctly:
Equality Assertions
<?php
// Assert two values are equal
$this->assertEquals(expected, actual);
$this->assertEquals(5, $calculator->add(2, 3));
// Assert identical (strict comparison with ===)
$this->assertSame(expected, actual);
$this->assertSame('5', $result); // Type matters!
// Assert not equal
$this->assertNotEquals(5, $calculator->add(2, 2));
?>
Boolean Assertions
<?php
// Assert true
$this->assertTrue($user->isActive());
// Assert false
$this->assertFalse($user->isBanned());
// Assert null
$this->assertNull($user->deletedAt);
// Assert not null
$this->assertNotNull($user->createdAt);
?>
Type Assertions
<?php
// Assert instance of class
$this->assertInstanceOf(User::class, $user);
// Assert is array
$this->assertIsArray($users);
// Assert is string
$this->assertIsString($name);
// Assert is numeric
$this->assertIsNumeric($age);
?>
String Assertions
<?php
// Assert string contains substring
$this->assertStringContainsString('hello', $message);
// Assert string starts with
$this->assertStringStartsWith('http://', $url);
// Assert string ends with
$this->assertStringEndsWith('.com', $domain);
// Assert string matches regex
$this->assertMatchesRegularExpression('/^[A-Z]/', $name);
?>
Array Assertions
<?php
// Assert array has key
$this->assertArrayHasKey('email', $user);
// Assert array contains value
$this->assertContains('admin', $roles);
// Assert array count
$this->assertCount(3, $users);
// Assert empty array
$this->assertEmpty($errors);
?>
Exception Assertions
<?php
public function test_division_by_zero_throws_exception()
{
$this->expectException(DivisionByZeroError::class);
$this->expectExceptionMessage('Division by zero');
$calculator = new Calculator();
$calculator->divide(10, 0);
}
?>
Important: expectException() must be called before the code that throws the exception, not after!
Test Fixtures: Setup and Teardown
PHPUnit provides hooks for preparing and cleaning up test environments:
<?php
class UserTest extends TestCase
{
private $user;
// Runs once before all tests in the class
public static function setUpBeforeClass(): void
{
// Initialize shared resources
DB::connect();
}
// Runs before each test method
protected function setUp(): void
{
parent::setUp();
$this->user = new User([
'name' => 'John Doe',
'email' => 'john@example.com'
]);
}
// Runs after each test method
protected function tearDown(): void
{
$this->user = null;
parent::tearDown();
}
// Runs once after all tests in the class
public static function tearDownAfterClass(): void
{
DB::disconnect();
}
public function test_user_has_name()
{
$this->assertEquals('John Doe', $this->user->name);
}
}
?>
Data Providers
Data providers allow you to run the same test with different inputs:
<?php
class MathTest extends TestCase
{
/**
* @dataProvider additionProvider
*/
public function test_addition($a, $b, $expected)
{
$calculator = new Calculator();
$result = $calculator->add($a, $b);
$this->assertEquals($expected, $result);
}
public static function additionProvider(): array
{
return [
'positive numbers' => [2, 3, 5],
'negative numbers' => [-2, -3, -5],
'mixed numbers' => [-2, 5, 3],
'with zero' => [0, 5, 5],
];
}
}
?>
This single test method runs four times with different data sets!
Pro Tip: Use array keys in data providers to give meaningful names to each test case. They appear in test output, making failures easier to identify.
Test Dependencies
Sometimes tests depend on the results of other tests:
<?php
class StackTest extends TestCase
{
public function test_push_and_pop()
{
$stack = [];
$stack[] = 'foo';
$this->assertSame('foo', array_pop($stack));
$this->assertEmpty($stack);
return $stack;
}
/**
* @depends test_push_and_pop
*/
public function test_empty_after_pop($stack)
{
$this->assertEmpty($stack);
}
}
?>
Use Sparingly: Test dependencies can make test suites brittle. Use them only when absolutely necessary.
Skipping and Incomplete Tests
<?php
class FeatureTest extends TestCase
{
public function test_requires_database()
{
if (!extension_loaded('pdo_mysql')) {
$this->markTestSkipped(
'MySQL extension not available'
);
}
// Test code here
}
public function test_incomplete_feature()
{
$this->markTestIncomplete(
'This test is not yet implemented'
);
}
}
?>
Testing Protected/Private Methods
While you should generally test public interfaces, sometimes you need to test private methods:
<?php
class UserTest extends TestCase
{
public function test_private_validation()
{
$user = new User();
// Use reflection to access private method
$reflection = new ReflectionClass($user);
$method = $reflection->getMethod('validateEmail');
$method->setAccessible(true);
$result = $method->invoke($user, 'invalid-email');
$this->assertFalse($result);
}
}
?>
Best Practice: If you find yourself testing private methods frequently, it might indicate a design issue. Consider extracting logic to a separate, testable class.
Test Output
PHPUnit provides detailed test output:
PHPUnit 10.5.5 by Sebastian Bergmann and contributors.
Runtime: PHP 8.2.0
Configuration: phpunit.xml
..F.. 5 / 5 (100%)
Time: 00:00.123, Memory: 10.00 MB
There was 1 failure:
1) CalculatorTest::test_division
Failed asserting that 2 matches expected 3.
tests/Unit/CalculatorTest.php:25
FAILURES!
Tests: 5, Assertions: 8, Failures: 1.
Symbols in output:
. - Successful test
F - Failed assertion
E - Error (exception thrown)
S - Skipped test
I - Incomplete test
Code Coverage
PHPUnit can analyze which code lines are executed by tests:
# Generate HTML coverage report
./vendor/bin/phpunit --coverage-html coverage
# Generate text coverage summary
./vendor/bin/phpunit --coverage-text
# Require minimum coverage
./vendor/bin/phpunit --coverage-text --coverage-filter app
Requirement: Code coverage requires Xdebug or PCOV PHP extension to be installed.
Practice Exercise
Create a StringHelper class and write PHPUnit tests for it:
<?php
class StringHelper
{
public static function reverse(string $str): string
{
return strrev($str);
}
public static function capitalize(string $str): string
{
return ucfirst($str);
}
public static function truncate(string $str, int $length): string
{
if (strlen($str) <= $length) {
return $str;
}
return substr($str, 0, $length) . '...';
}
}
?>
Your Task:
- Create
StringHelperTest.php
- Write at least 2 tests for each method
- Test edge cases (empty strings, very long strings)
- Use data providers for the truncate method
- Run tests and ensure 100% code coverage
Summary
PHPUnit is a powerful and flexible testing framework for PHP. With its rich assertion library, test organization features, and tools like data providers and code coverage, PHPUnit makes writing and maintaining tests straightforward. In the next lesson, we'll explore Jest, the JavaScript equivalent of PHPUnit.
Key Takeaways:
- PHPUnit is the industry standard for PHP testing
- Tests extend
TestCase and methods start with test
- Use setUp/tearDown for test fixtures
- Rich assertion library covers most testing needs
- Data providers enable testing multiple scenarios efficiently
- Code coverage helps identify untested code