Полное руководство по тестированию PHP кода для разработчиков

Раздел: Разработка PHP -> Отладка и тестирование

Основные подходы к тестированию PHP кода

Тестирование PHP кода позволяет гарантировать корректную работу приложений, упрощает рефакторинг и предотвращает регрессии. Ниже рассмотрены основные методы и инструменты.

Как наиболее эффективно тестировать PHP код?

Наиболее распространённым и функциональным решением является использование PHPUnit – фреймворка для модульного тестирования. Он поддерживает assertions, data providers, mock-объекты и интеграцию с покрытием кода.

Установка и настройка

composer require --dev phpunit/phpunit

Test 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 4
class 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.

Тестирование PHP кода - comments

En
Test php code (php)