Тестирование приложений на PHP: от основ до сложных решений
Введение в тестирование PHP
Тестирование кода на PHP помогает выявлять ошибки на ранних этапах и облегчает рефакторинг. Рассматриваются основные подходы и инструменты, начиная с самого популярного решения - PHPUnit.
Основное решение: PHPUnit
PHPUnit является стандартом де-факто для юнит-тестирования в PHP. Оно обеспечивает написание и выполнение тестов, поддерживает mock-объекты, поставщиков данных и многие другие возможности.
Установка через Composer
composer require --dev phpunit/phpunit:^11.0Http 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());
}
}
Такой подход изолирует тесты друг от друга.