Testing & TDD

PHPUnit Basics

18 min Lesson 4 of 35

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:

  1. Create StringHelperTest.php
  2. Write at least 2 tests for each method
  3. Test edge cases (empty strings, very long strings)
  4. Use data providers for the truncate method
  5. 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