Testing in PHP: Unit, Integration, E2E — A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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)');                

Blogs

Book Recommendations