Skip to content
Learni
View all tutorials
Tests & Qualité

How to Write Effective Tests with PHPUnit in 2026

14 minINTERMEDIATE
Lire en français

Introduction

PHPUnit is the reference tool for unit testing in PHP. In a professional setting, well-written tests guarantee code stability during refactoring and updates. This intermediate tutorial shows you how to go beyond basic assertions to create maintainable, fast, and reliable test suites. We will cover advanced configuration, intelligent use of mocks, and data providers to avoid code duplication.

Prerequisites

  • PHP 8.2 or higher
  • Composer installed
  • Basic knowledge of OOP and unit testing
  • Existing PHP project (Laravel, Symfony, or vanilla)

Installation via Composer

terminal
composer require --dev phpunit/phpunit ^11.0
./vendor/bin/phpunit --version

This command installs the latest stable version of PHPUnit as a development dependency. Always verify the version to ensure compatibility with your plugins and extensions.

PHPUnit Configuration File

phpunit.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
         displayDetailsOnTestsThatTriggerWarnings="true">
    <testsuites>
        <testsuite name="Unit">
            <directory>tests/Unit</directory>
        </testsuite>
    </testsuites>
    <source>
        <include>
            <directory>src</directory>
        </include>
    </source>
</phpunit>

This file defines test paths, enables colors, and includes only source code in coverage reports. It prevents accidentally running integration tests.

Complete First Unit Test

tests/Unit/UserTest.php
<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Models\User;
use PHPUnit\Framework\TestCase;

final class UserTest extends TestCase
{
    public function testFullNameIsCorrectlyFormatted(): void
    {
        $user = new User('Jean', 'Dupont');
        $this->assertSame('Jean Dupont', $user->getFullName());
    }
}

A simple and readable test that verifies business logic. Use assertSame instead of assertEquals for strict type comparisons.

Creating an Advanced Mock

tests/Unit/PaymentServiceTest.php
<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Services\PaymentService;
use App\Services\Logger;
use PHPUnit\Framework\TestCase;

final class PaymentServiceTest extends TestCase
{
    public function testPaymentIsLoggedOnSuccess(): void
    {
        $logger = $this->createMock(Logger::class);
        $logger->expects($this->once())
               ->method('info')
               ->with($this->stringContains('Payment successful'));

        $service = new PaymentService($logger);
        $service->processPayment(100.0);
    }
}

The mock isolates the service under test. expects($this->once()) ensures the method is called exactly once, quickly detecting regressions.

Data Provider for Parameterized Tests

tests/Unit/PriceCalculatorTest.php
<?php

declare(strict_types=1);

namespace Tests\Unit;

use App\Services\PriceCalculator;
use PHPUnit\Framework\TestCase;

final class PriceCalculatorTest extends TestCase
{
    /**
     * @dataProvider priceProvider
     */
    public function testCalculatePrice(float $base, float $tax, float $expected): void
    {
        $calculator = new PriceCalculator();
        $this->assertSame($expected, $calculator->calculate($base, $tax));
    }

    public static function priceProvider(): array
    {
        return [
            [100.0, 0.20, 120.0],
            [50.0, 0.10, 55.0],
            [0.0, 0.20, 0.0],
        ];
    }
}

Data providers eliminate code duplication and allow easy testing of many edge cases with a single test method.

Best Practices

  • Name your tests according to expected behavior (testXXXWhenYYYThenZZZ)
  • Keep each test independent with no side effects
  • Use fixtures or factories for test data
  • Run tests in parallel whenever possible with --parallel
  • Measure coverage but do not treat it as a goal

Common Mistakes to Avoid

  • Testing implementations instead of behaviors
  • Forgetting to reset mocks between tests
  • Using assertEquals on objects without __toString
  • Ignoring randomly failing tests (flaky tests)

Going Further

Discover our advanced training on PHP testing and software architecture: https://learni-group.com/formations