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.
PHP testing with PHPUnit is how professional developers verify their code works correctly by writing automated tests that run assertions against functions and classes. This tutorial teaches you to install PHPUnit, write test cases, use data providers, mock dependencies, and measure Code Coverage so you can ship reliable PHP 8 applications with confidence.
What You'll Learn
- Installing PHPUnit via Composer and configuring phpunit.xml
- Writing test classes that extend TestCase and using setUp/tearDown
- Using assertions like assertEquals, assertTrue, assertCount, and assertThrows
- Creating data providers for parameterized tests
- Using test doubles and mock objects to isolate dependencies
- Measuring and interpreting code coverage reports
Why It Matters
Manual testing cannot keep up with the complexity of modern web applications. Every time you deploy a new feature, you risk breaking existing functionality. PHPUnit gives you a safety net: write tests once, run them in seconds, and catch regressions before they reach production. In team environments, tests also serve as living documentation that describes how each component should behave.
Real-World Use
The Durga Antivirus Pro team runs over 10,000 PHPUnit tests on every build. When a new malware signature parser is added, existing tests verify that file scanning, signature matching, and quarantine workflows still function correctly. Without automated tests, a single regression could ship a broken update to millions of users.
Learning Path
flowchart LR A[Dependency Injection] --> B[Testing with PHPUnit\nYou are here] B --> C[Debugging] style B fill:#f90,color:#fff
Installing PHPUnit
PHPUnit is installed as a Composer dev dependency. Open your terminal and run:
composer require --dev phpunit/phpunit ^11
After installation, create a phpunit.xml file in your project root:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Application">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Run the test suite with:
vendor/bin/phpunit
Output:
PHPUnit 11.0.0 by Sebastian Bergmann and contributors.
. 1 / 1 (100%)
Time: 00:00.015, Memory: 6.00 MB
OK (1 test, 1 assertion)
Writing Your First Test
Create a tests/ directory and add a test for a simple Calculator class.
First, the class under test in src/Calculator.php:
<?php
namespace App;
class Calculator
{
public function add(float $a, float $b): float
{
return $a + $b;
}
public function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new \InvalidArgumentException('Division by zero is not allowed');
}
return $a / $b;
}
}
Now the test in tests/CalculatorTest.php:
<?php
namespace App\Test;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
public function testAddReturnsCorrectSum(): void
{
$result = $this->calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testDivideReturnsCorrectQuotient(): void
{
$result = $this->calculator->divide(10, 2);
$this->assertEquals(5, $result);
}
public function testDivideByZeroThrowsException(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->calculator->divide(10, 0);
}
}
Output of vendor/bin/phpunit:
PHPUnit 11.0.0 by Sebastian Bergmann and contributors.
... 3 / 3 (100%)
Time: 00:00.018, Memory: 6.00 MB
OK (3 tests, 3 assertions)
Using Data Providers
Data providers let you run the same test with multiple inputs. Add this method to CalculatorTest.php:
<?php
namespace App\Test;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
private Calculator $calculator;
protected function setUp(): void
{
$this->calculator = new Calculator();
}
/** @dataProvider additionProvider */
public function testAddWithMultipleInputs(float $a, float $b, float $expected): void
{
$this->assertEquals($expected, $this->calculator->add($a, $b));
}
public static function additionProvider(): array
{
return [
[1, 1, 2],
[2.5, 3.5, 6.0],
[-1, 1, 0],
[0, 0, 0],
[100, 200, 300],
];
}
}
Output:
PHPUnit 11.0.0 by Sebastian Bergmann and contributors.
........ 8 / 8 (100%)
Time: 00:00.020, Memory: 6.00 MB
OK (8 tests, 8 assertions)
Mocking Dependencies
When a class depends on an external service, mock that dependency to control its behavior. Consider a Mailer class that sends emails:
<?php
namespace App;
interface MailerInterface
{
public function send(string $to, string $subject, string $body): bool;
}
class NewsletterService
{
public function __construct(private MailerInterface $mailer) {}
public function sendWelcome(string $email): bool
{
return $this->mailer->send(
$email,
'Welcome to the platform',
'Thank you for signing up!'
);
}
}
Test the NewsletterService by mocking the MailerInterface:
<?php
namespace App\Test;
use App\MailerInterface;
use App\NewsletterService;
use PHPUnit\Framework\TestCase;
class NewsletterServiceTest extends TestCase
{
public function testSendWelcomeReturnsTrueOnSuccess(): void
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->method('send')
->with('user@example.com', 'Welcome to the platform', 'Thank you for signing up!')
->willReturn(true);
$service = new NewsletterService($mailer);
$result = $service->sendWelcome('user@example.com');
$this->assertTrue($result);
}
}
Code Coverage
PHPUnit can measure which lines of your code are exercised by tests. Enable coverage in phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Application">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory>src</directory>
</include>
</coverage>
</phpunit>
Run with coverage:
vendor/bin/phpunit --coverage-html coverage
Open coverage/index.html in a browser to see which methods are tested and which are not.
Common Mistakes
- Not using setUp for shared state: Creating fresh instances in setUp prevents test pollution. If one test modifies shared state, other tests may fail unpredictably.
- Ignoring the red-green-refactor cycle: Write a failing test first, make it pass, then refactor. Skipping the red step means you never verify the test can actually catch a failure.
- Testing implementation details: Test public behavior, not private methods. If you refactor internals, tests should still pass. Mock interfaces, not classes.
- Mocking everything: Over-mocking makes tests brittle and hard to read. Use real objects for value objects and simple classes; mock only expensive or unpredictable dependencies.
- Not running tests in CI: Tests that only pass on your local machine provide no safety net. Run PHPUnit in a continuous integration pipeline on every push.
Practice Questions
What is the difference between
assertEqualsandassertSamein PHPUnit?assertEqualschecks value equality using==, whileassertSamechecks identity with===. UseassertSamewhen object identity or type strictness matters.
How do you mark a test as skipped or incomplete?
- Use
$this->markTestSkipped('Reason')or$this->markTestIncomplete('Reason')inside the test method.
- Use
What is the purpose of the
@dataProviderannotation?- It allows a test method to receive multiple sets of arguments from a separate provider method, eliminating repetitive test code.
How do you test that an exception is thrown?
- Call
$this->expectException(ExceptionClass::class)before the code that should throw.
- Call
Challenge: Write a test suite for a
UserValidatorclass that checks email format, password length (minimum 8 characters), and username uniqueness against a Repository. Use mocks for the repository.
Mini Project
Create a PHPUnit test suite for a simple shopping cart application:
- Write an
Itemclass withname,price, andquantityproperties. - Write a
Cartclass that can add items, remove items, calculate the total, and apply a discount. - Write tests covering: adding items, removing items, empty cart total, discount calculation, negative quantity rejection, and duplicate item handling.
- Measure code coverage and aim for 100% line coverage.
FAQ
What's Next
Now that you can write tests, learn how to debug failing code efficiently in the next lesson on Debugging.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro