Тестирование PHP приложений: от юнит до функциональных тестов
Обзор систем тестирования PHP
Основное решение: PHPUnit – это де-факто стандарт для модульного тестирования в PHP. Он предоставляет гибкий набор утверждений, фикстуры, методы для организации тестов и интеграцию с CI/CD. Ниже рассмотрены базовые принципы работы с PHPUnit.
Для установки PHPUnit используйте Composer (требуется PHP 7.4+). Пример команды:
composer require --dev phpunit/phpunitUnit tests php (юнит-тестирование php)
Структура проекта обычно предполагает размещение тестов в каталоге tests/ и использование автозагрузки PSR-4. Минимальный тест:
<?php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAddition()
{
$calc = new Calculator();
$this->assertEquals(4, $calc->add(2, 2));
}
}
система тестирования php (система тестирования на php)
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 0.001s, Memory: 6.00 MB OK (1 test, 1 assertion)
Api tests php (тестирование api на php)
Цель использования: обеспечить регрессионное тестирование отдельных компонентов (классов, функций) с высокой скоростью и повторяемостью. Сценарии: TDD, CI проверки перед деплоем, покрытие кода.
Типичная ошибка: метод теста не начинается с test или не имеет аннотации @test. PHPUnit не распознает его как тест и пропускает. Решение: явно указывать префикс или использовать атрибут #[\PHPUnit\Framework\Attributes\Test] в PHP 8.
Еще одна проблема: зависимость от внешних ресурсов (БД, API) без изоляции. При каждом запуске теста данные должны быть предсказуемы. Решение: применять тестовые заглушки (mock-объекты) или фикстуры в setUp().
Как написать параметризованные тесты с PHPUnit Data Providers?
Для проверки одной функции на множестве входных данных используйте Data Provider – метод, возвращающий массив наборов. Пример:
<?php
class CalculatorTest extends TestCase
{
/** @dataProvider additionProvider */
public function testAddition($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, -2],
];
}
}
Http localhost test php (тестирование php на localhost)
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. ... 3 / 3 (100%) Time: 0.001s, Memory: 6.00 MB OK (3 tests, 3 assertions)
Php testing (тестирование php)
Цель: сократить дублирование кода и охватить граничные случаи. Используется, когда логика однотипна, но данные различны.
Проблема: если Data Provider возвращает слишком большой массив, тесты становятся медленными. Решение: для интеграционных тестов ограничить количество кейсов, а для unit – допустимо много.
Как организовать функциональное тестирование с эмуляцией браузера с помощью Codeception?
Codeception позволяет писать acceptance-тесты, имитирующие клики по кнопкам, заполнение форм и проверку ответа сервера. Установка:
composer require --dev codeception/codeception
Инициализация для веб-приложения:
vendor/bin/codecept bootstrap
Пример acceptance-теста для логина:
<?php
$I = new AcceptanceTester($scenario);
$I->amOnPage('/login');
$I->fillField('username', 'admin');
$I->fillField('password', 'secret');
$I->click('Login');
$I->see('Dashboard');
Цель: проверка пользовательских сценариев без реального браузера (через PhpBrowser или Guzzle). Часто используется для smoke-тестов после деплоя.
Частая ошибка: Codeception не видит элементы на странице из-за JavaScript. Решение: включить модуль WebDriver (Selenium) для тестов с JS.
Как описать поведение класса через спецификации с PHPSpec?
PHPSpec следует методологии BDD: сначала пишется спецификация (что класс должен делать), затем код. Установка:
composer require --dev phpspec/phpspec
Создание спецификации для класса User:
<?php
class UserSpec extends ObjectBehavior
{
function it_should_set_full_name()
{
$this->setFirstName('John');
$this->setLastName('Doe');
$this->getFullName()->shouldReturn('John Doe');
}
}
Затем генерируется класс User через vendor/bin/phpspec run.
Цель: разработка через поведение (BDD), когда спецификация становится документацией. Подходит для моделей предметной области.
Ошибка: PHPSpec требует строгой структуры папок и имен. Если спецификация не найдена, проверьте соглашение {ClassName}Spec в каталоге spec/.
Как использовать SimpleTest для быстрого прототипирования тестов?
SimpleTest старее PHPUnit, но иногда быстрее разворачивается (без Composer). Библиотека лежит как отдельный файл. Пример:
<?php
require_once 'simpletest/autorun.php';
class TestCalculator extends UnitTestCase
{
function testAddition() {
$calc = new Calculator();
$this->assertEqual(4, $calc->add(2, 2));
}
}
Цель: минималистичное тестирование без Composer, например, для легаси-проектов.
Проблема: SimpleTest не обновляется, отсутствует поддержка современных возможностей PHP 8. Лучше мигрировать на PHPUnit.
Расширенные примеры тестирования
Интеграционное тестирование с PHPUnit и SQLite
Для тестирования запросов к БД без внешней базы данных используйте SQLite in-memory. Пример теста для репозитория UserRepository:
<?php
use PHPUnit\Framework\TestCase;
class UserRepositoryTest extends TestCase
{
private PDO $pdo;
private UserRepository $repository;
protected function setUp(): void
{
$this->pdo = new PDO('sqlite::memory:');
$this->pdo->exec('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)');
$this->repository = new UserRepository($this->pdo);
}
public function testFindByName()
{
$this->pdo->exec("INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')");
$user = $this->repository->findByName('Alice');
$this->assertEquals('Alice', $user->getName());
}
public function testInsertGeneratesId()
{
$newUser = new User('Bob', 'bob@example.com');
$id = $this->repository->insert($newUser);
$this->assertIsInt($id);
$this->assertGreaterThan(0, $id);
}
}
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. .. 2 / 2 (100%) Time: 0.002s, Memory: 6.50 MB OK (2 tests, 2 assertions)
Пояснение: метод setUp() создает чистую таблицу для каждого теста. После выполнения теста SQLite в памяти уничтожается. Это изолирует тесты друг от друга.
Тестирование API с Codeception
Допустим, ваше приложение отдает JSON по адресу /api/users/1. Acceptance-тест с модулем REST:
<?php
class ApiCest
{
public function tryToGetUser(ApiTester $I)
{
$I->sendGET('/api/users/1');
$I->seeResponseCodeIs(200);
$I->seeResponseIsJson();
$I->seeResponseContainsJson(['id' => 1, 'name' => 'Alice']);
}
public function tryToCreateUser(ApiTester $I)
{
$I->sendPOST('/api/users', ['name' => 'Bob', 'email' => 'bob@example.com']);
$I->seeResponseCodeIs(201);
$I->seeResponseIsJson();
$I->seeResponseMatchesJsonType([
'id' => 'integer:>0',
'name' => 'string',
]);
}
}
vendor/bin/codecept run acceptance Time: 1.23 s, Memory: 10.00 MB 2 passed
Пояснение: модуль REST автоматически эмулирует HTTP запросы. Можно тестировать как REST, так и SOAP (модулем PhpBrowser).
Тестирование с использованием заглушек (Mocking) в PHPUnit
Заглушки заменяют реальные зависимости для изоляции теста. Пример теста для сервиса, который отправляет email:
<?php
class MailServiceTest extends TestCase
{
public function testSendWelcomeEmailCallsMailer()
{
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with($this->stringContains('welcome'));
$service = new WelcomeEmailService($mailer);
$service->sendTo(new User('Alice', 'alice@example.com'));
}
}
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 0.001s, Memory: 6.00 MB OK (1 test, 1 assertion)
Пояснение: метод createMock создает объект-заглушку, проверяющую вызов send с нужным аргументом. Это позволяет тестировать логику сервиса без реальной отправки писем.
Нестандартный пример: тестирование исключений в PHPUnit
Убедитесь, что код выбрасывает исключение при неверном вводе.
<?php
class CalculatorTest extends TestCase
{
public function testDivideByZeroThrowsException()
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Division by zero is not allowed');
$calc = new Calculator();
$calc->divide(10, 0);
}
}
PHPUnit 10.0.0 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 0.001s, Memory: 6.00 MB OK (1 test, 1 assertion)
Пояснение: вызов expectException регистрирует ожидание. Тест считается пройденным, если исключение было выброшено.