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
composer require --dev phpunit/phpunit ^11.0
./vendor/bin/phpunit --versionThis 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
<?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
<?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
<?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
<?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