Introduction: Why Testing in PHP is Needed
Development in PHP is not just about writing code that "works". In modern web development, application stability and predictability come to the forefront. Without testing, any change in the code risks breaking already working functionality, and manually searching for errors turns into an endless "bug hunt". This is exactly where testing comes to the rescue.
In this article, we will break down three key levels of testing PHP applications: Unit, Integration, and E2E (End-to-End). You will learn how they differ, which tools to use, and how to build an effective testing strategy for your project.
Unit Tests: The Foundation of the Pyramid
Unit tests are the fastest and most isolated level of testing. Their goal is to verify the operation of a single "unit" (class, method, or function) in isolation from external dependencies: database, file system, API.
Tools for PHP
The most popular framework is PHPUnit. A more modern alternative is Pest, which offers a more concise syntax.
Example of a Unit Test in PHPUnit
Let's say we have a simple calculator class:
<?php
// src/Calculator.php
class Calculator
{
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;
}
}
The test for it will look like this:
<?php
// tests/Unit/CalculatorTest.php
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 testDivideThrowsExceptionOnZero(): void
{
$this->expectException(\\InvalidArgumentException::class);
$this->calculator->divide(10, 0);
}
}
Key Principles of Unit Tests:
- Isolation: all external dependencies (DB, API) are replaced with mocks or stubs.
- Speed: they execute in milliseconds.
- They verify one specific scenario (one assert per test is good practice).
Run command: ./vendor/bin/phpunit tests/Unit
Integration Tests: Checking Interaction
Integration tests go one level higher. They check how different parts of the system work together: a class with a database, a controller with a model, a service with an external API (real or simulated).
When are Integration Tests Needed?
- Checking the correctness of SQL queries.
- Validating the connection between application layers (e.g., Repository + Entity).
- Testing middleware or events.
Example: Testing a Repository with a Real Database
For integration tests, a test database is often used (SQLite in memory or transactions in PostgreSQL). Let's look at an example using PHPUnit and a trait for resetting the database:
<?php
// tests/Integration/UserRepositoryTest.php
use PHPUnit\\Framework\\TestCase;
class UserRepositoryTest extends TestCase
{
private PDO $pdo;
private UserRepository $repository;
protected function setUp(): void
{
// Create an in-memory SQLite for isolation
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');