Pruebas de código en PHP: guía detallada
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты написал программу для школьного дневника. Она должна уметь: ставить оценки, считать средний балл, выводить расписание. Ты написал код, запустил — и всё работает. Но что будет, если пользователь введёт не число, а букву? Или попросит вывести оценку за день, который ещё не наступил? Твоя программа может «упасть» или выдать чушь.
Тестирование кода — это как проверка домашнего задания перед тем, как сдать его учителю. Ты не просто смотришь на код, а запускаешь специальные проверки, которые автоматически убеждаются, что каждая функция работает правильно.
Представь, что ты печёшь пирожки. Ты же не подашь гостям сырое тесто? Ты проверяешь: пропёкся ли пирожок, не подгорел ли, вкусная ли начинка. Вот и код нужно «попробовать» — протестировать.
В реальной жизни тестирование помогает:
- Не сломать старое, когда добавляешь новое. Ты изменил одну строчку — а перестало работать 10 функций. Тесты это заметят.
- Быстрее находить ошибки. Вместо того чтобы вручную вводить данные 100 раз, ты запускаешь одну команду.
- Думать как пользователь. Ты начинаешь понимать, что программа должна делать в любой ситуации.
Теория: как это работает изнутри
В программировании есть разные виды тестов. Мы сосредоточимся на юнит-тестах (unit tests) — это проверка одной маленькой части программы: функции или метода.
Идея простая: ты пишешь код, который вызывает твою функцию и сравнивает результат с ожидаемым. Если совпало — тест пройден. Если нет — тест упал, и ты знаешь, где искать проблему.
В PHP есть специальные библиотеки для тестирования. Самая популярная — PHPUnit. Она работает так:
- Ты создаёшь класс-тест, который наследуется от
PHPUnitFrameworkTestCase. - Внутри пишешь методы, названия которых начинаются на
test. - В каждом методе используешь специальные функции-«утверждения» (assertions), которые проверяют условия.
Например, есть функция сложения. Ты пишешь тест: «вызови функцию с числами 2 и 3 — проверь, что результат равен 5». Если функция вернёт 5 — тест зелёный. Если вернёт 6 — красный.
Важно: тесты не доказывают, что программа правильная на 100%. Они доказывают, что программа работает так, как ты ожидаешь в тех случаях, которые проверил.
Процесс тестирования обычно выглядит так:
- Red — пишешь тест, который падает (потому что функции ещё нет).
- Green — пишешь минимальный код, чтобы тест прошёл.
- Refactor — улучшаешь код, не меняя его поведения.
Это называется TDD — Test-Driven Development (разработка через тестирование).
Синтаксис в PHP: все формы и варианты
Для работы с PHPUnit тебе нужно:
- Установить PHPUnit через Composer:
composer require --dev phpunit/phpunit - Создать файл с тестом.
- Запустить:
./vendor/bin/phpunit tests/
Вот основные «утверждения» (assertions), которые ты будешь использовать:
| Метод | Что проверяет | Пример |
|---|---|---|
assertEquals($expected, $actual) |
Равенство значений (с приведением типов) | $this->assertEquals(5, sum(2,3)); |
assertSame($expected, $actual) |
Строгое равенство (тип + значение) | $this->assertSame(5, sum(2,3)); |
assertTrue($condition) |
Условие истинно | $this->assertTrue($user->isAdmin()); |
assertFalse($condition) |
Условие ложно | $this->assertFalse($user->isBlocked()); |
assertNull($variable) |
Переменная равна null | $this->assertNull($result); |
assertCount($count, $array) |
Количество элементов в массиве | $this->assertCount(3, $items); |
assertContains($value, $array) |
Массив содержит значение | $this->assertContains('apple', $fruits); |
assertInstanceOf($class, $object) |
Объект принадлежит классу | $this->assertInstanceOf(User::class, $user); |
Также есть методы для проверки исключений:
expectException(ExceptionClass::class)— ожидаем, что будет выброшено исключение.expectExceptionMessage('Message')— проверяем текст сообщения.
Примеры кода — от простого к сложному
Пример 1: Простейший тест функции сложения
<?php
// Файл: src/Math.php
// Простая функция для сложения двух чисел
function sum(int $a, int $b): int {
return $a + $b;
}
<?php
// Файл: tests/MathTest.php
// Подключаем PHPUnit
use PHPUnitFrameworkTestCase;
// Подключаем нашу функцию
require_once __DIR__ . '/../src/Math.php';
class MathTest extends TestCase {
// Метод теста — имя начинается с "test"
public function testSumWithPositiveNumbers(): void {
// Вызываем функцию с числами 2 и 3
$result = sum(2, 3);
// Проверяем: результат должен быть равен 5
$this->assertEquals(5, $result);
// Если всё верно — тест зелёный
}
}
Объяснение: Мы создали класс MathTest, который наследуется от TestCase. Внутри — метод testSumWithPositiveNumbers. Он вызывает функцию sum и проверяет, что результат равен 5. Если функция работает правильно — тест пройден.
Пример 2: Проверка нескольких случаев
<?php
// tests/MathTest.php — добавляем новые методы
class MathTest extends TestCase {
public function testSumWithPositiveNumbers(): void {
$this->assertEquals(5, sum(2, 3));
}
// Тест с нулём
public function testSumWithZero(): void {
$this->assertEquals(5, sum(5, 0));
$this->assertEquals(0, sum(0, 0));
}
// Тест с отрицательными числами
public function testSumWithNegativeNumbers(): void {
$this->assertEquals(0, sum(-5, 5));
$this->assertEquals(-10, sum(-5, -5));
}
}
Объяснение: Один тест может проверять несколько случаев. Мы проверяем сложение с нулём и отрицательными числами. Если хоть одна проверка не пройдёт — весь тест упадёт.
Пример 3: Тестирование класса User
<?php
// src/User.php
class User {
private string $name;
private bool $isAdmin = false;
public function __construct(string $name) {
$this->name = $name;
}
public function getName(): string {
return $this->name;
}
public function makeAdmin(): void {
$this->isAdmin = true;
}
public function isAdmin(): bool {
return $this->isAdmin;
}
}
<?php
// tests/UserTest.php
use PHPUnitFrameworkTestCase;
require_once __DIR__ . '/../src/User.php';
class UserTest extends TestCase {
public function testUserCreation(): void {
// Создаём нового пользователя
$user = new User('Иван');
// Проверяем имя
$this->assertEquals('Иван', $user->getName());
// Проверяем, что он не админ по умолчанию
$this->assertFalse($user->isAdmin());
}
public function testMakeAdmin(): void {
$user = new User('Петя');
// Делаем пользователя админом
$user->makeAdmin();
// Проверяем, что он стал админом
$this->assertTrue($user->isAdmin());
}
}
Объяснение: Мы тестируем объект. Проверяем, что при создании пользователь не админ, а после вызова makeAdmin() — становится. Это типичный тест для класса.
Пример 4: Проверка исключений
<?php
// src/Calculator.php
class Calculator {
// Функция деления — выбрасывает исключение при делении на ноль
public function divide(int $a, int $b): float {
if ($b === 0) {
throw new InvalidArgumentException('Деление на ноль невозможно');
}
return $a / $b;
}
}
<?php
// tests/CalculatorTest.php
use PHPUnitFrameworkTestCase;
require_once __DIR__ . '/../src/Calculator.php';
class CalculatorTest extends TestCase {
public function testDivideNormal(): void {
$calc = new Calculator();
$result = $calc->divide(10, 2);
$this->assertEquals(5.0, $result);
}
public function testDivideByZero(): void {
$calc = new Calculator();
// Говорим PHPUnit: ожидаем исключение
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Деление на ноль невозможно');
// Этот вызов должен выбросить исключение
$calc->divide(10, 0);
}
}
Объяснение: Мы проверяем, что при делении на ноль выбрасывается исключение с правильным сообщением. Если исключение не выбросится — тест упадёт.
Пример 5: Тестирование с setUp (подготовка данных)
<?php
// tests/UserTest.php — улучшенная версия
use PHPUnitFrameworkTestCase;
require_once __DIR__ . '/../src/User.php';
class UserTest extends TestCase {
private User $user;
// Этот метод выполняется перед каждым тестом
protected function setUp(): void {
$this->user = new User('Тестовый пользователь');
}
public function testDefaultName(): void {
$this->assertEquals('Тестовый пользователь', $this->user->getName());
}
public function testDefaultIsNotAdmin(): void {
$this->assertFalse($this->user->isAdmin());
}
public function testAfterMakeAdmin(): void {
$this->user->makeAdmin();
$this->assertTrue($this->user->isAdmin());
}
}
Объяснение: Метод setUp() выполняется перед каждым тестом. Это удобно: не нужно создавать объект в каждом тесте заново. Если тестов много — это экономит время и делает код чище.
Пример 6: Data Providers (наборы данных)
<?php
// tests/MathTest.php — используем Data Provider
use PHPUnitFrameworkTestCase;
require_once __DIR__ . '/../src/Math.php';
class MathTest extends TestCase {
/**
* @dataProvider sumDataProvider
*/
public function testSum(int $a, int $b, int $expected): void {
$this->assertEquals($expected, sum($a, $b));
}
// Метод, который возвращает наборы данных
public function sumDataProvider(): array {
return [
'positive numbers' => [2, 3, 5],
'with zero' => [5, 0, 5],
'negative numbers' => [-5, 5, 0],
'two negatives' => [-3, -7, -10],
'large numbers' => [1000, 2000, 3000],
];
}
}
Объяснение: Data Provider позволяет запустить один тест с разными данными. Мы передаём массивы: первый элемент — первый аргумент, второй — второй, третий — ожидаемый результат. Тест запустится 5 раз с разными данными.
Пример 7: Mock-объекты (имитация)
<?php
// src/Mailer.php
class Mailer {
public function send(string $email, string $message): bool {
// В реальности — отправка email
return true;
}
}
// src/UserService.php
class UserService {
private Mailer $mailer;
public function __construct(Mailer $mailer) {
$this->mailer = $mailer;
}
public function welcomeNewUser(string $email): string {
$this->mailer->send($email, 'Добро пожаловать!');
return 'Письмо отправлено на ' . $email;
}
}
<?php
// tests/UserServiceTest.php
use PHPUnitFrameworkTestCase;
require_once __DIR__ . '/../src/Mailer.php';
require_once __DIR__ . '/../src/UserService.php';
class UserServiceTest extends TestCase {
public function testWelcomeNewUser(): void {
// Создаём "заглушку" для Mailer
$mailerMock = $this->createMock(Mailer::class);
// Говорим: когда вызовут метод send, верни true
$mailerMock->method('send')->willReturn(true);
// Создаём сервис с заглушкой
$service = new UserService($mailerMock);
// Вызываем метод
$result = $service->welcomeNewUser('test@example.com');
// Проверяем результат
$this->assertEquals('Письмо отправлено на test@example.com', $result);
}
}
Объяснение: Mock-объекты нужны, когда не хочется по-настоящему отправлять email или работать с базой данных. Мы создаём «заглушку», которая имитирует поведение настоящего объекта. Это ускоряет тесты и делает их независимыми от внешних сервисов.
Сравнение с Python: чем похоже и чем отличается
В Python тоже есть тестирование, и оно очень похоже на PHP. Вот основные отличия:
| Характеристика | PHP (PHPUnit) | Python (unittest / pytest) |
|---|---|---|
| Основная библиотека | PHPUnit | unittest (встроенная), pytest |
| Синтаксис утверждений | $this->assertEquals() |
self.assertEqual() или assert |
| Data Providers | Аннотация @dataProvider |
Декоратор @pytest.mark.parametrize |
| Mock-объекты | $this->createMock() |
unittest.mock.Mock() |
| Запуск тестов | ./vendor/bin/phpunit |
python -m unittest или pytest |
| Именование тестов | Методы с префиксом test |
Методы с префиксом test_ |
Похоже:
- И там, и там тесты — это классы с методами.
- Идея «красный-зелёный-рефакторинг» одинакова.
- Есть setUp/tearDown для подготовки данных.
Отличия:
- В Python тесты можно писать без классов (в pytest).
- Python использует отступы вместо фигурных скобок.
- В Python больше встроенных инструментов для тестирования.
Типичные ошибки и подводные камни
- Тестировать слишком много в одном тесте. Один тест — одна проверка. Если тест упал, ты должен сразу понять, что именно сломалось.
- Забывать про граничные случаи. Пустые массивы, нули, отрицательные числа, null — всё это нужно проверять.
- Тестировать внутреннюю реализацию, а не поведение. Тест должен проверять, что функция делает, а не как она это делает. Иначе при рефакторинге тесты сломаются.
- Не запускать тесты перед коммитом. Сделал изменение — запусти тесты. Если они зелёные — можно коммитить.
- Писать тесты после того, как код уже написан. Лучше писать тесты до или одновременно с кодом. Так ты сразу видишь, что код работает.
- Игнорировать медленные тесты. Если тест выполняется 10 секунд — его будут редко запускать. Оптимизируй или выноси в отдельную группу.
- Тестировать только «счастливый путь». Проверяй не только когда всё хорошо, но и когда приходят плохие данные.
Запомни: тест, который никогда не падает, бесполезен. Если тест всегда зелёный — возможно, он ничего не проверяет.
Лучшие практики и советы профессионалов
- Пиши тесты до кода (TDD). Это дисциплинирует и заставляет думать о том, что должна делать программа.
- Один тест — одна проверка. Используй понятные имена:
testUserCannotRegisterWithEmptyEmail. - Изолируй тесты. Каждый тест должен быть независимым. Не полагайся на порядок выполнения.
- Используй Data Providers для повторяющихся проверок. Это уменьшает дублирование кода.
- Тестируй границы. Если функция принимает числа от 1 до 100 — проверь 0, 1, 50, 100, 101.
- Не тестируй чужой код. Не нужно проверять, работает ли PHP-функция
array_push()— она уже протестирована разработчиками PHP. - Поддерживай тесты в чистоте. Если тест сложный — упрости его. Тесты — это тоже код, за которым нужно ухаживать.
- Запускай тесты автоматически. Настрой CI/CD (например, GitHub Actions), чтобы тесты запускались при каждом коммите.
- Пиши тесты для багов. Нашёл ошибку? Сначала напиши тест, который её воспроизводит. Потом исправляй. Так баг больше не вернётся.
- Используй покрытие кода (code coverage). PHPUnit показывает, какие строки кода были выполнены во время тестов. Стремись к 80%+.
Вопросы для самопроверки
-
Что такое юнит-тест?
Ответ: Это автоматическая проверка одной маленькой части программы (функции или метода), которая сравнивает ожидаемый результат с фактическим.
-
Какой метод PHPUnit используется для проверки равенства значений?
Ответ:
assertEquals($expected, $actual). Он проверяет, что два значения равны (с приведением типов). -
Что такое Data Provider в PHPUnit?
Ответ: Это метод, который возвращает наборы данных для одного теста. Тест запускается несколько раз с разными аргументами.
-
Зачем нужны Mock-объекты?
Ответ: Чтобы имитировать поведение сложных или внешних объектов (база данных, API, почта). Это делает тесты быстрыми и независимыми.
-
Что означает «красный-зелёный-рефакторинг»?
Ответ: Это цикл TDD: сначала пишешь тест, который падает (красный), потом пишешь код, чтобы тест прошёл (зелёный), потом улучшаешь код (рефакторинг).
-
Как проверить, что функция выбрасывает исключение?
Ответ: Использовать
$this->expectException(ExceptionClass::class)перед вызовом функции, которая должна выбросить исключение. -
Почему не стоит тестировать внутреннюю реализацию?
Ответ: Потому что при рефакторинге (изменении внутреннего устройства) тесты сломаются, даже если поведение осталось правильным. Тесты должны проверять поведение, а не код.
-
Что такое code coverage?
Ответ: Это метрика, показывающая, какой процент кода выполняется во время тестов. Чем выше — тем больше кода проверено.
-
Какой метод выполняется перед каждым тестом?
Ответ:
setUp(). Он используется для подготовки данных, которые нужны в каждом тесте. -
Что делать, если нашёл баг в коде?
Ответ: Сначала написать тест, который воспроизводит этот баг (он упадёт). Потом исправить код, чтобы тест прошёл. Так баг не вернётся в будущем.
Итог и что изучить дальше
Тестирование — это не роскошь, а необходимость. Оно превращает программирование из «напиши и молись, что работает» в «напиши и будь уверен». Ты научился:
- Писать юнит-тесты на PHP с помощью PHPUnit.
- Использовать утверждения (assertEquals, assertTrue, assertFalse).
- Проверять исключения и использовать Data Providers.
- Создавать Mock-объекты для изоляции тестов.
- Понимать разницу между PHP и Python в тестировании.
Что изучить дальше:
- Интеграционные тесты — проверка, как разные части системы работают вместе (например, с базой данных).
- Функциональные тесты — проверка целых сценариев (например, регистрация пользователя через веб-интерфейс).
- BDD (Behavior-Driven Development) — тестирование на естественном языке с помощью Behat.
- CI/CD — настройка автоматического запуска тестов при каждом коммите (GitHub Actions, GitLab CI).
- Продвинутые техники — тестирование баз данных, API, асинхронного кода.
Помни: хороший программист — это не тот, кто не делает ошибок, а тот, кто умеет их быстро находить и исправлять. Тестирование — твой главный инструмент для этого.