Testing in PHP: unit, integration, e2e from A to Z

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 not an option, but a standard

In modern PHP development, testing has long ceased to be a "nice to have" and has become a mandatory stage of production projects. Without code coverage by tests, you risk releasing an error into production that could cost the business money and reputation. In this article, we will break down three levels of testing: unit, integration, and e2e (end-to-end). You will learn how they differ, which tools to use (PHPUnit, Pest, Laravel Dusk, Codeception), and how to build a proper testing strategy for your PHP project.



1. Unit tests: testing isolated units of code

Unit tests check the operation of individual classes, functions, or methods in isolation from external dependencies (DB, API, file system). This is the fastest and cheapest type of test. The main tool in PHP is PHPUnit (the de facto standard) and its "trendy" alternative Pest.



1.1. Installation and first test with PHPUnit

Install PHPUnit via Composer:

composer require --dev phpunit/phpunit

Create a test for a simple calculator class:

<?php// src/Calculator.phpclass Calculator {    public function add(int $a, int $b): int {        return $a + $b;    }}

// tests/CalculatorTest.phpuse PHPUnit\\Framework\\TestCase;

class CalculatorTest extends TestCase { public function testAddition(): void { $calc = new Calculator(); $result = $calc->add(2, 3); $this->assertEquals(5, $result); }}

Run the test:

./vendor/bin/phpunit tests


1.2. Using Mock objects

To isolate the class under test, we "replace" its dependencies using mocks. Example with a user repository:

<?phpclass UserService {    private UserRepository $repo;    public function __construct(UserRepository $repo) {        $this->repo = $repo;    }    public function getUserName(int $id): string {        $user = $this->repo->findById($id);        return $user ? $user->getName() : 'Guest';    }}

// In the test$mockRepo = $this->createMock(UserRepository::class);$mockRepo->method('findById') ->with(1) ->willReturn(new User('Alice'));

$service = new UserService($mockRepo);$this->assertEquals('Alice', $service->getUserName(1));

Best practice: Write unit tests for all business logic. They should execute in seconds and not depend on the environment.



2. Integration tests: checking interaction with DB, API, and external services

Integration tests check how your classes work together with real components: database, cache, external HTTP services. They are slower than unit tests but provide more confidence that the system works as a whole.



2.1. Testing database interaction (Laravel + RefreshDatabase)

If you use Laravel, integration tests become very simple thanks to the RefreshDatabase trait:

<?phpnamespace Tests\\Feature;

use Tests\\TestCase;use Illuminate\\Foundation\\Testing\\RefreshDatabase;

class UserRegistrationTest extends TestCase { use RefreshDatabase;

public function test_user_can_register(): void { $response = $this->post('/register', [ 'name' => 'John', 'email' => 'john@example.com', 'password' => 'secret123', ]);

$response->assertStatus(302); // redirect after success $this->assertDatabaseHas('users', [ 'email' => 'john@example.com' ]); }}

The RefreshDatabase trait automatically migrates the database before tests and

Blogs

Book Recommendations