Тестирование разработки на PHP
Основы модульного тестирования с PHPUnit
Эффективным решением для тестирования PHP кода является использование PHPUnit. Это стандарт де факто для модульных тестов в экосистеме PHP. Установка выполняется через Composer:
composer require --dev phpunit/phpunit
Пример простого теста для класса Calculator:
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAddition()
{
$calc = new Calculator();
$result = $calc->add(2, 3);
$this->assertEquals(5, $result);
}
}
Запуск тестов осуществляется командой:
vendor/bin/phpunit tests/CalculatorTest.php
PHPUnit предоставляет множество утверждений (assertEquals, assertTrue, assertNull и т.д.), а также возможности для организации тестов в наборы, использования провайдеров данных и тегирования.
Альтернативные подходы включают:
- PHPT - простые файлы сценариев для тестирования расширений PHP, часто используются для проверки поведения функций.
- SimpleTest - более легковесный фреймворк, хорош для небольших проектов, но менее популярен.
- Behat - инструмент поведенчески-ориентированного тестирования (BDD), позволяет писать тесты на естественном языке с поддержкой контекстов.
- Codeception - надстройка над PHPUnit, добавляющая модули для тестирования БД, веб-интерфейсов и API, удобна для acceptance-тестов.
- Mockery / Prophecy - библиотеки для создания заглушек и подмены объектов, часто используются совместно с PHPUnit.
Типичные проблемы и их решения:
- Класс не найден - ошибка автозагрузки. Проверьте composer.json на наличие автозагрузки PSR-4 и выполните composer dump-autoload.
- Тестирование protected/private методов - используйте рефлексию или выносите логику в отдельные публичные методы. Пример через ReflectionMethod:
$method = new ReflectionMethod('ClassName', 'methodName');
$method->setAccessible(true);
$result = $method->invoke($object, $args);
protected function setUp(): void
{
parent::setUp();
$this->connection->beginTransaction();
}
protected function tearDown(): void
{
$this->connection->rollBack();
parent::tearDown();
}
Расширенные примеры тестирования
Тестирование с mock-объектами
Использование Mockery для имитации внешнего сервиса:
use Mockery as m;
class MailerTest extends TestCase
{
public function testSendWelcomeEmail()
{
$mailer = m::mock('Mailer');
$mailer->shouldReceive('send')
->once()
->with('user@example.com', 'Welcome')
->andReturn(true);
$service = new UserRegistration($mailer);
$result = $service->register('user@example.com');
$this->assertTrue($result);
}
protected function tearDown(): void
{
m::close();
}
}
OK (1 test, 1 assertion)
Тестирование исключений
Проверка, что метод выбрасывает исключение при некорректных данных:
public function testDivisionByZero()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Division by zero is not allowed');
$calc = new Calculator();
$calc->divide(10, 0);
}
OK (1 test, 2 assertions)
Провайдеры данных
Многократное выполнение теста с разными наборами параметров:
/**
* @dataProvider additionProvider
*/
public function testAdd($a, $b, $expected)
{
$calc = new Calculator();
$this->assertEquals($expected, $calc->add($a, $b));
}
public function additionProvider(): array
{
return [
[0, 0, 0],
[1, 2, 3],
[-1, 1, 0],
[2.5, 3.7, 6.2],
];
}
OK (4 tests, 4 assertions)
Тестирование HTTP-запросов (Guzzle + PHPUnit)
Использование mock-клиента для эмуляции ответа API:
use GuzzleHttp\Client;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;
public function testFetchUsers()
{
$mock = new MockHandler([
new Response(200, [], json_encode(['users' => [['id' => 1]]])),
]);
$handlerStack = HandlerStack::create($mock);
$client = new Client(['handler' => $handlerStack]);
$service = new UserService($client);
$users = $service->getUsers();
$this->assertCount(1, $users);
$this->assertEquals(1, $users[0]->id);
}
OK (1 test, 2 assertions)
Функциональное тестирование Symfony с Panther
Panther позволяет выполнять тесты в реальном браузере:
use Symfony\Component\Panther\PantherTestCase;
class LoginTest extends PantherTestCase
{
public function testLoginPage()
{
$client = static::createPantherClient();
$crawler = $client->request('GET', '/login');
$form = $crawler->selectButton('Login')->form([
'_username' => 'admin',
'_password' => 'admin123',
]);
$client->submit($form);
$this->assertPageContainsText('Dashboard');
}
}
OK (1 test, 1 assertion)
Тестирование кэша с мокированием времени
Использование Carbon для фиксации времени:
use Carbon\Carbon;
public function testCacheExpiration()
{
Carbon::setTestNow(Carbon::create(2023, 1, 1, 12, 0, 0));
$cache = new CacheService();
$cache->put('key', 'value', 10); // ttl 10 minutes
$this->assertEquals('value', $cache->get('key'));
Carbon::setTestNow(Carbon::create(2023, 1, 1, 12, 11, 0)); // 11 minutes later
$this->assertNull($cache->get('key'));
Carbon::setTestNow(); // reset
}
OK (1 test, 2 assertions)
Измерение покрытия кода
Генерация отчёта о покрытии с помощью Xdebug и PHPUnit:
phpunit --coverage-html coverage
Результат - HTML-папка coverage с детализацией по строками, методам и классам.