Тестирование приложений на PHP: от основ до сложных решений

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

Введение в тестирование PHP

Тестирование кода на PHP помогает выявлять ошибки на ранних этапах и облегчает рефакторинг. Рассматриваются основные подходы и инструменты, начиная с самого популярного решения - PHPUnit.

Основное решение: PHPUnit

PHPUnit является стандартом де-факто для юнит-тестирования в PHP. Оно обеспечивает написание и выполнение тестов, поддерживает mock-объекты, поставщиков данных и многие другие возможности.

Установка через Composer

composer require --dev phpunit/phpunit:^11.0

Http localhost test php (тестирование php на localhost)

После установки создаётся директория vendor и файл phpunit.xml для конфигурации.

Написание простого теста

Создаётся класс теста, наследующий PHPUnit\Framework\TestCase.

<?php
use PHPUnit\Framework\TestCase;

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

Test php (тестирование php)

Тестируемый класс Calculator предполагается в отдельном файле с автозагрузкой.

Запуск тестов

./vendor/bin/phpunit tests/CalculatorTest.php

Api tests php (тестирование api на php)

Результат вывода:

PHPUnit 11.5.0
.                                                                   1 / 1 (100%)

Time: 0.002 seconds, Memory: 6.00 MB

OK (1 test, 1 assertion)

Типичные проблемы и их решения

  • Ошибка автозагрузки. Если класс не найден, проверьте настройки composer.json (autoload) и выполните composer dump-autoload.
  • Тест не выполняется. Убедитесь, что файл заканчивается на Test.php и лежит в директории, указанной в phpunit.xml.
  • Неверное ожидание. Используйте правильный assertion, например assertTrue вместо assertEquals с булевым значением.

Варианты решений

Как настроить тесты с использованием Codeception?

Codeception - фреймворк для приёмочного и функционального тестирования. Поддерживает модули для PHPBrowser, REST, Doctrine и др.

composer require --dev codeception/codeception:^5.0
vendor/bin/codecept bootstrap

Создаётся структура tests/acceptance, tests/functional, tests/unit. Пример unit-теста с Codeception:

<?php
class CalculatorCest
{
    public function tryToTestAddition(UnitTester $I)
    {
        $calculator = new Calculator();
        $I->assertEquals(5, $calculator->add(2, 3));
    }
}

Запуск: vendor/bin/codecept run unit

Частая ошибка: несоответствие версий модулей. Решение - актуализировать composer.json и выполнить composer update.

Как проверить исключения с помощью PHPUnit?

Используется метод expectException() или атрибут #[\PHPUnit\Framework\Attributes\ExpectException].

<?php
use PHPUnit\Framework\TestCase;

class ExceptionTest extends TestCase
{
    public function testDivisionByZero()
    {
        $this->expectException(\InvalidArgumentException::class);
        $this->expectExceptionMessage('Division by zero');
        
        $calculator = new Calculator();
        $calculator->divide(10, 0);
    }
}

Проблема: ожидаемое исключение не выбрасывается. Проверьте, что код внутри метода действительно кидает исключение, а не только выводит сообщение.

Как выполнить тесты с mock-объектами для изоляции зависимостей?

Mock-объекты создаются через createMock() или getMockBuilder().

<?php
use PHPUnit\Framework\TestCase;

class UserServiceTest extends TestCase
{
    public function testSendWelcomeEmail()
    {
        $mailer = $this->createMock(Mailer::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 вызывается ровно один раз с правильными аргументами и возвращает true.

Ошибка: неверный подсчёт вызовов. Используйте expects($this->exactly(1)) или $this->never().

Как протестировать REST API с помощью PHPUnit и Guzzle?

Для функционального тестирования API можно использовать GuzzleHttp в тестовом окружении.

<?php
use PHPUnit\Framework\TestCase;
use GuzzleHttp\Client;

class ApiTest extends TestCase
{
    private $client;
    
    protected function setUp(): void
    {
        $this->client = new Client(['base_uri' => 'http://localhost:8000']);
    }
    
    public function testGetUsers()
    {
        $response = $this->client->get('/api/users');
        $this->assertEquals(200, $response->getStatusCode());
        $data = json_decode($response->getBody(), true);
        $this->assertIsArray($data);
        $this->assertArrayHasKey('data', $data);
    }
}

Запускается вместе с тестовым веб-сервером, например, через php -S localhost:8000.

Проблема: тесты зависят от состояния базы данных. Решение - изолировать тесты с помощью транзакций или in-memory БД.

Выбор инструмента зависит от целей: для модульного тестирования удобен PHPUnit, для приёмочных сценариев подходит Codeception, а для API-тестов можно использовать Guzzle.

Расширенные примеры тестирования

1. Использование поставщиков данных (Data Providers)

Позволяет запускать один тест с разными наборами данных.

Пример
<?php
use PHPUnit\Framework\TestCase;

class DataProviderTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $calculator = new Calculator();
        $this->assertEquals($expected, $calculator->add($a, $b));
    }
    
    public function additionProvider(): array
    {
        return [
            'positive numbers' => [1, 2, 3],
            'with zero' => [0, 5, 5],
            'negative' => [-1, -2, -3],
        ];
    }
}

Результат при запуске:

PHPUnit 11.5.0
...

3 / 3 (100%)

2. Тестирование приватных методов через Reflection

Хотя рекомендуется тестировать публичный интерфейс, иногда требуется проверить внутреннюю логику. Используется рефлексия.

Пример
<?php
use PHPUnit\Framework\TestCase;

class PrivateMethodTest extends TestCase
{
    public function testPrivateMethod()
    {
        $object = new SomeClass();
        $reflection = new ReflectionMethod(SomeClass::class, 'privateMethod');
        $reflection->setAccessible(true);
        $result = $reflection->invoke($object, 'secret');
        $this->assertEquals('processed_secret', $result);
    }
}

3. Тестирование с зависимостью от базы данных (SQLite in-memory)

Пример интеграционного теста с PDO и SQLite в памяти.

Пример
<?php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    private $pdo;
    
    protected function setUp(): void
    {
        $this->pdo = new PDO('sqlite::memory:');
        $this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)');
        $this->pdo->exec("INSERT INTO users (name) VALUES ('Alice')");
    }
    
    public function testFetchUser()
    {
        $stmt = $this->pdo->query('SELECT name FROM users WHERE id = 1');
        $user = $stmt->fetch(PDO::FETCH_ASSOC);
        $this->assertEquals('Alice', $user['name']);
    }
}

Результат: тест проходит, так как база чистая для каждого теста (в setUp).

4. Тестирование с использованием @depends (зависимости между тестами)

Позволяет передавать результат одного теста в другой.

Пример
<?php
use PHPUnit\Framework\TestCase;

class DependsTest extends TestCase
{
    public function testCreateUser()
    {
        $user = ['id' => 1, 'name' => 'Bob'];
        return $user;
    }
    
    /**
     * @depends testCreateUser
     */
    public function testUserName($user)
    {
        $this->assertEquals('Bob', $user['name']);
    }
}

Порядок выполнения: сначала testCreateUser, затем testUserName с полученным значением.

5. Тестирование вывода (output buffering)

Проверка, что скрипт выводит правильный текст.

Пример
<?php
use PHPUnit\Framework\TestCase;

class OutputTest extends TestCase
{
    public function testGreetingOutput()
    {
        $greeting = new Greeter();
        ob_start();
        $greeting->sayHello('World');
        $output = ob_get_clean();
        $this->assertEquals('

Hello, World!

', $output); } }

6. Тестирование консольных команд Symfony

Для приложений на Symfony используется класс KernelTestCase и CommandTester.

Пример
<?php
namespace App\Tests\Command;

use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;

class MyCommandTest extends KernelTestCase
{
    public function testExecute()
    {
        $kernel = static::createKernel();
        $application = new Application($kernel);
        
        $command = $application->find('app:my-command');
        $commandTester = new CommandTester($command);
        $commandTester->execute([
            'argument' => 'value'
        ]);
        
        $output = $commandTester->getDisplay();
        $this->assertStringContainsString('Success', $output);
    }
}

7. Тестирование с использованием fixtures и транзакций

Пример с Doctrine Fixtures и откатом транзакции после каждого теста.

Пример
<?php
use Doctrine\ORM\EntityManager;
use PHPUnit\Framework\TestCase;

class EntityTest extends TestCase
{
    private $em;
    
    protected function setUp(): void
    {
        // подключаем EntityManager с in-memory БД
        $this->em = EntityManager::create(...);
        $this->em->beginTransaction();
        // загружаем fixtures (например, с помощью ORMSetup)
    }
    
    protected function tearDown(): void
    {
        $this->em->rollback();
    }
    
    public function testPersistUser()
    {
        $user = new User('test@example.com');
        $this->em->persist($user);
        $this->em->flush();
        $this->assertNotNull($user->getId());
    }
}

Такой подход изолирует тесты друг от друга.

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

En
Test php (php)