PHP Testing with PHPUnit — Complete Guide to Automated Testing
In this tutorial, you will learn about PHP Testing with PHPUnit. We cover key concepts, practical examples, and best practices to help you master this topic.
PHPUnit is the industry-standard testing framework for PHP, enabling developers to write unit tests, integration tests, and achieve high code coverage with assertions and test doubles.
What You'll Learn
By the end of this tutorial, you'll install and configure PHPUnit, write unit tests with assertions, use data providers, create mocks and stubs, measure code coverage, and integrate testing into CI pipelines.
Why Testing Matters
Testing catches bugs before deployment, documents behavior, prevents regressions, and gives confidence during Refactoring. PHPUnit is used by Laravel, Symfony, and thousands of projects.
Real-World Use
A payment processing service runs 2000+ PHPUnit tests before every deployment. A failed test blocks the release. Developers identify and fix regressions in minutes instead of hours.
Testing Learning Path
flowchart LR
A[MVC] --> B[REST API]
B --> C[JWT Auth]
C --> D[Testing]
D --> E[Security]
C --> F{You Are Here}
style F fill:#f90,color:#fff
Installation
composer require --dev phpunit/phpunit
./vendor/bin/phpunit --version
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
</phpunit>
Writing Tests
<?php
// src/Math.php
namespace App;
class Math {
public function add(int $a, int $b): int { return $a + $b; }
public function divide(int $a, int $b): float {
if ($b === 0) throw new \InvalidArgumentException("Division by zero");
return $a / $b;
}
}
<?php
// tests/Unit/MathTest.php
namespace Tests\Unit;
use App\Math;
use PHPUnit\Framework\TestCase;
class MathTest extends TestCase {
private Math $math;
protected function setUp(): void {
$this->math = new Math();
}
public function testAddReturnsSum(): void {
$result = $this->math->add(2, 3);
$this->assertEquals(5, $result);
}
public function testAddWithNegativeNumbers(): void {
$this->assertEquals(-1, $this->math->add(2, -3));
}
public function testDivideThrowsOnZero(): void {
$this->expectException(\InvalidArgumentException::class);
$this->math->divide(10, 0);
}
}
Data Providers
<?php
class MathTest extends TestCase {
/** @dataProvider additionProvider */
public function testAdd(int $a, int $b, int $expected): void {
$this->assertEquals($expected, (new Math())->add($a, $b));
}
public static function additionProvider(): array {
return [
"positive numbers" => [1, 2, 3],
"negative numbers" => [-1, -2, -3],
"mixed signs" => [5, -3, 2],
"with zero" => [0, 5, 5],
];
}
}
Test Doubles (Mocks)
<?php
// src/Mailer.php
namespace App;
interface Mailer {
public function send(string $to, string $subject, string $body): bool;
}
// src/UserService.php
namespace App;
class UserService {
public function __construct(private Mailer $mailer) {}
public function register(string $email): bool {
// ... create user ...
return $this->mailer->send($email, "Welcome", "Thanks for joining!");
}
}
<?php
class UserServiceTest extends TestCase {
public function testRegisterSendsWelcomeEmail(): void {
$mailer = $this->createMock(Mailer::class);
$mailer->expects($this->once())
->method("send")
->with("user@example.com", "Welcome", "Thanks for joining!")
->willReturn(true);
$service = new UserService($mailer);
$result = $service->register("user@example.com");
$this->assertTrue($result);
}
}
Code Coverage
./vendor/bin/phpunit --coverage-html coverage/
./vendor/bin/phpunit --coverage-text --min-coverage=80
<phpunit>
<coverage>
<include>
<directory>src</directory>
</include>
<report>
<html outputDirectory="coverage"/>
<text outputFile="php://stdout"/>
</report>
</coverage>
</phpunit>
Running Tests
./vendor/bin/phpunit
./vendor/bin/phpunit tests/Unit/MathTest.php
./vendor/bin/phpunit --filter testAdd
./vendor/bin/phpunit --testdox # Human-readable output
Common Mistakes
1. Testing Too Many Things in One Test
Each test should verify one behavior. Multiple assertions in one test make it hard to identify failures.
2. Not Using setUp()
Duplicating setup code across tests violates DRY. Use setUp() to create shared dependencies.
3. Testing the Framework or Database
Don't test PHP itself or your database queries. Mock external dependencies and test your logic.
4. Ignoring Edge Cases
Test empty strings, zero values, null inputs, and boundary conditions — not just the happy path.
5. Not Running Tests in CI
Tests only protect code when run automatically. Integrate PHPUnit into your CI/CD pipeline.
Practice Questions
1. What is the difference between unit and integration tests?
Unit tests isolate a single class. Integration tests verify multiple components work together (database, API).
2. How do you mock a database connection?
Use createMock() to create a test double that returns predefined results without hitting the database.
3. What is a data provider in PHPUnit?
A static method annotated with @dataProvider that returns arrays of arguments. Each array runs the test once.
4. How do you measure code coverage?
Run phpunit with --coverage-html or --coverage-text flags. Aim for 80%+ coverage on business logic.
5. Challenge: Write PHPUnit tests for a Calculator class with add, subtract, multiply, divide methods.
<?php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase {
private Calculator $calc;
protected function setUp(): void { $this->calc = new Calculator(); }
public function testAdd(): void { $this->assertEquals(5, $this->calc->add(2, 3)); }
public function testSubtract(): void { $this->assertEquals(3, $this->calc->subtract(7, 4)); }
public function testMultiply(): void { $this->assertEquals(12, $this->calc->multiply(3, 4)); }
public function testDivide(): void { $this->assertEquals(5, $this->calc->divide(10, 2)); }
public function testDivideByZero(): void {
$this->expectException(\InvalidArgumentException::class);
$this->calc->divide(10, 0);
}
}
class Calculator {
public function add(int $a, int $b): int { return $a + $b; }
public function subtract(int $a, int $b): int { return $a - $b; }
public function multiply(int $a, int $b): int { return $a * $b; }
public function divide(int $a, int $b): float {
if ($b === 0) throw new \InvalidArgumentException("Division by zero");
return $a / $b;
}
}
FAQ
Mini Project: Test User Registration
Write PHPUnit tests for a complete user registration flow.
<?php
use PHPUnit\Framework\TestCase;
class RegistrationTest extends TestCase {
public function testSuccessfulRegistration(): void {
$mailer = $this->createMock(Mailer::class);
$mailer->method("send")->willReturn(true);
$repo = $this->createMock(UserRepository::class);
$repo->method("findByEmail")->willReturn(null);
$repo->method("save")->willReturn(1);
$service = new RegistrationService($repo, $mailer);
$result = $service->register("test@example.com", "password123");
$this->assertTrue($result);
}
public function testDuplicateEmailFails(): void {
$repo = $this->createMock(UserRepository::class);
$repo->method("findByEmail")->willReturn(["id" => 1]);
$service = new RegistrationService($repo, $this->createMock(Mailer::class));
$this->expectException(\RuntimeException::class);
$service->register("existing@example.com", "pass");
}
}
What's Next
PHP Security PHP Performance PHP Docker Deployment
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro