Полное руководство по тестированию PHP кода для разработчиков
Основные подходы к тестированию PHP кода
Тестирование PHP кода позволяет гарантировать корректную работу приложений, упрощает рефакторинг и предотвращает регрессии. Ниже рассмотрены основные методы и инструменты.
Как наиболее эффективно тестировать PHP код?
Наиболее распространённым и функциональным решением является использование PHPUnit – фреймворка для модульного тестирования. Он поддерживает assertions, data providers, mock-объекты и интеграцию с покрытием кода.
Установка и настройка
composer require --dev phpunit/phpunitTest php code (тестирование php кода)
После установки создаётся конфигурационный файл phpunit.xml для указания директории с тестами и настроек.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="Unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>src</directory>
</whitelist>
</filter>
</phpunit>Написание первого теста
<?php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAdd()
{
$this->assertSame(4, (new Calculator())->add(2, 2));
}
}PHPUnit 9.5.10 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 0.006 seconds
Типичные проблемы и решения
- Ошибка автозагрузки классов: Убедитесь, что файл
vendor/autoload.phpподключён в bootstrap, а классы тестируемого кода соответствуют PSR-4. - Несовместимость версий PHP и PHPUnit: Используйте PHPUnit 9 для PHP 7.3+ или PHPUnit 10/11 для PHP 8.x.
- Медленные тесты с базой данных: Применяйте транзакции для отката изменений после каждого теста или используйте встроенные средства PHPUnit для работы с БД (например,
DatabaseTestCase).
Как протестировать функции без фреймворка с помощью assert?
В PHP встроена конструкция assert(), позволяющая проверять условия. Это простой способ для быстрых проверок, но он не подходит для сложных проектов из-за отсутствия отчётов и структуры.
<?php
assert(1 + 1 === 2);
echo "Assertion passed";
?>Assertion passed
Проблема:
По умолчанию assert может быть отключён в production. Включается черезzend.assertions = 1 в php.ini. Не даёт детальной информации об ошибках.Какие альтернативы PHPUnit существуют для модульного тестирования?
SimpleTest – легковесный фреймворк, не требующий composer, но устаревший и менее гибкий.
<?php
require_once('simpletest/autorun.php');
class TestOfCalculator extends UnitTestCase {
function testAdd() {
$this->assertEqual(4, (new Calculator())->add(2,2));
}
}
?>Codeception – надстройка над PHPUnit, упрощающая тестирование API, UI и acceptance тесты.
# acceptance.suite.yml
actor: AcceptanceTester
modules:
enabled:
- PhpBrowser:
url: http://localhostОшибка:
При использовании Codeception без правильной установки драйвера браузера (например, WebDriver) acceptance-тесты падают. Решение: установитьcodeception/module-webdriver.Как тестировать поведение приложения с помощью BDD?
Behat – инструмент для Behaviour-Driven Development, позволяющий писать сценарии на естественном языке (Gherkin).
Feature: Calculator
Scenario: Add two numbers
Given I have a calculator
When I add 2 and 2
Then I should get 4class FeatureContext implements Context
{
private $calculator;
private $result;
/** @Given I have a calculator */
public function iHaveACalculator()
{
$this->calculator = new Calculator();
}
/** @When I add :arg1 and :arg2 */
public function iAddAnd($arg1, $arg2)
{
$this->result = $this->calculator->add($arg1, $arg2);
}
/** @Then I should get :arg1 */
public function iShouldGet($arg1)
{
if ($this->result !== (int)$arg1) {
throw new Exception("Expected $arg1 but got {$this->result}");
}
}
}Проблема:
Behat требует создания контекстов и поддержки большого количества шагов, что увеличивает объём кода. Для сложных сценариев может быть трудно поддерживать читаемость.Расширенные примеры тестирования PHP кода
Как протестировать исключения и ошибки?
<?php
class DivisionByZeroException extends \Exception {}
class Math
{
public function divide($a, $b)
{
if ($b == 0) {
throw new DivisionByZeroException('Division by zero');
}
return $a / $b;
}
}
class MathTest extends TestCase
{
public function testDivideByZeroThrowsException()
{
$this->expectException(DivisionByZeroException::class);
$this->expectExceptionMessage('Division by zero');
(new Math())->divide(10, 0);
}
public function testDivide()
{
$this->assertSame(5.0, (new Math())->divide(10, 2));
}
}PHPUnit 9.5.10 .. 2 / 2 (100%)
Как использовать data providers для параметризованных тестов?
<?php
class StringUtilsTest extends TestCase
{
/**
* @dataProvider provideStrings
*/
public function testStrlen($expected, $input)
{
$this->assertSame($expected, strlen($input));
}
public function provideStrings(): array
{
return [
[0, ''],
[1, 'a'],
[3, 'abc'],
[5, 'привет'], // UTF-8: strlen считает байты, ожидаем 10?
];
}
}PHPUnit 9.5.10 ..F. 1) StringUtilsTest::testStrlen with data set #3 (10, 'привет') Failed asserting that 12 matches expected 10. ... Ошибка: strlen для 'привет' (UTF-8) возвращает 12, а не 10. Решение: использовать mb_strlen.
Как создать mock-объекты для изоляции зависимостей?
<?php
interface MailerInterface
{
public function send(string $to, string $subject): bool;
}
class UserService
{
private $mailer;
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
public function register(string $email): bool
{
// ... сохранение пользователя ...
return $this->mailer->send($email, 'Welcome');
}
}
class UserServiceTest extends TestCase
{
public function testRegisterSendsEmail()
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with('user@example.com', 'Welcome')
->willReturn(true);
$service = new UserService($mailer);
$result = $service->register('user@example.com');
$this->assertTrue($result);
}
}Тест проходит успешно. Если метод send не вызывается – тест падает с сообщением о невыполненном ожидании.
Как тестировать код, работающий с базой данных, с транзакциями?
<?php
class UserRepositoryTest extends TestCase
{
private static $pdo;
private $connection;
public static function setUpBeforeClass(): void
{
self::$pdo = new PDO('sqlite::memory:');
self::$pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
}
protected function setUp(): void
{
$this->pdo->beginTransaction();
}
protected function tearDown(): void
{
$this->pdo->rollBack();
}
public function testInsertUser()
{
$stmt = $this->pdo->prepare('INSERT INTO users (name) VALUES (?)');
$stmt->execute(['Alice']);
$this->assertEquals(1, $this->pdo->query('SELECT COUNT(*) FROM users')->fetchColumn());
}
}Тест проходит, так как транзакция откатывается после setUp/tearDown, данные не сохраняются между тестами.
Как измерить покрытие кода тестами?
vendor/bin/phpunit --coverage-html coverageПосле выполнения в папке coverage появится HTML-отчёт с детализацией по строкам. Для работы требуется расширение Xdebug или PCOV.
Проблемы:
- Если Xdebug не установлен, покрытие не будет собрано. Установите через
pecl install xdebugилиapt install php-xdebug. - Высокое потребление памяти при большом проекте. Отключите покрытие для папок vendor с помощью фильтра в phpunit.xml.