Рабочий процесс разработки через тестирование
Формируйте дизайн с помощью цикла «красный — зелёный — рефакторинг»
«Рабочий процесс разработки через тестирование» — бесплатный урок PHP Academy на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
TDD — инструмент проектирования
Разработку через тестирование часто представляют как технику тестирования, но её настоящая ценность — стимул к проектированию. Написав тест первым, Вы вынуждены определить публичный интерфейс объекта, его зависимости и контракт ещё до написания реализации. Тесты — побочный результат; продуктом становится более качественное проектирование.
Красный, зелёный, рефакторинг
Цикл состоит из трёх чётких этапов:
- Красный — напишите неудачный тест для следующего небольшого поведения. Он должен завершиться ошибкой по правильной причине.
- Зелёный — напишите минимальный код, достаточный для прохождения теста, даже если он выглядит некрасиво.
- Рефакторинг — улучшайте реализацию и тесты, сохраняя успешное прохождение.
Дисциплина важна: никогда не пропускайте этап «Красный» — иначе Вы можете тестировать ничего, и не создавайте лишнего на этапе «Зелёный».
Красный: сначала напишите неудачный тест
Мы создадим PriceCalculator, который применяет процентную скидку. Начните с теста поведения, которого пока не существует. При запуске он должен завершиться ошибкой, потому что класс ещё не определён, — это правильный результат этапа «Красный».
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_applies_a_percentage_discount(): void
{
$calc = new PriceCalculator();
// 100 with 10% off -> 90.0
self::assertSame(90.0, $calc->withDiscount(100.0, 10));
}
}
Зелёный: минимальный код для прохождения
Теперь напишите минимальную реализацию, которая позволит перейти к зелёному этапу. Не добавляйте правила округления, проверку данных или работу с валютами: пока нет неудачного теста, который этого требует.
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
return $amount - ($amount * $percent / 100);
}
}
$calc = new PriceCalculator();
var_dump($calc->withDiscount(100.0, 10)); // float(90)
Триангуляция для обобщения
Один тест можно пройти, жёстко задав единственный результат. Триангуляция — добавление второго, отличающегося примера — заставляет реализацию стать обобщённой. Добавьте случай, который невозможно подделать тривиальным решением, чтобы вывести настоящую формулу.
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
public function test_zero_percent_is_unchanged(): void
{
self::assertSame(50.0, (new PriceCalculator())->withDiscount(50.0, 0));
}
public function test_full_discount_is_free(): void
{
self::assertSame(0.0, (new PriceCalculator())->withDiscount(50.0, 100));
}
}
Граничные случаи как спецификация
В TDD новое требование — это новый неудачный тест. Предположим, отрицательные скидки недопустимы. Сначала напишите тест, ожидающий исключение: он завершится ошибкой, потому что пока ничего его не вызывает. Тест документирует контракт.
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorValidationTest extends TestCase
{
public function test_rejects_negative_discount(): void
{
$this->expectException(\InvalidArgumentException::class);
(new PriceCalculator())->withDiscount(100.0, -5);
}
}
Снова зелёный этап, затем рефакторинг
Добавьте проверку, чтобы новый тест прошёл, а затем уверенно выполните рефакторинг: существующие тесты обнаружат регрессии. Обратите внимание: округление мы также добавили только после того, как его обосновал тест или известное требование.
<?php
final class PriceCalculator
{
public function withDiscount(float $amount, float $percent): float
{
if ($percent < 0 || $percent > 100) {
throw new \InvalidArgumentException('percent must be 0..100');
}
return round($amount - ($amount * $percent / 100), 2);
}
}
var_dump((new PriceCalculator())->withDiscount(19.99, 15)); // float(16.99)
Поставщики данных сокращают примеры
Когда поведение стабилизируется, объедините множество пар примеров в один параметризованный тест с поставщиком данных. Это ускоряет цикл «Красный/зелёный» и сохраняет таблицу сценариев понятной.
<?php
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Attributes\DataProvider;
final class DiscountTableTest extends TestCase
{
#[DataProvider('cases')]
public function test_discounts(float $amount, float $pct, float $expected): void
{
self::assertSame($expected, (new PriceCalculator())->withDiscount($amount, $pct));
}
public static function cases(): array
{
return [
'no discount' => [100.0, 0, 100.0],
'ten percent' => [100.0, 10, 90.0],
'free' => [100.0, 100, 0.0],
];
}
}
Сохраняйте цикл быстрым
TDD работает только тогда, когда обратная связь занимает секунды, а не минуты. Практические приёмы:
- Запускайте один файл или применяйте фильтр:
phpunit --filter test_full_discount_is_free. - Используйте
--testdox, чтобы читать тесты как спецификацию поведения. - Не добавляйте операции ввода-вывода в модульные тесты: во внутреннем цикле не должно быть базы данных, сети или файловой системы.
vendor/bin/phpunit --testdox --filter PriceCalculatorРефакторьте и тесты
Этап рефакторинга охватывает весь набор тестов, а не только рабочий код. Устраняйте дублирование с помощью вспомогательных средств и setUp(), называйте тесты по проверяемому поведению и удаляйте тесты, которые больше не проверяют ничего значимого. Тесты — это код, который Вы будете поддерживать всегда, поэтому относитесь к ним так же внимательно.
<?php
use PHPUnit\Framework\TestCase;
final class PriceCalculatorTest extends TestCase
{
private PriceCalculator $calc;
protected function setUp(): void
{
$this->calc = new PriceCalculator(); // shared setup, no duplication
}
public function test_ten_percent(): void
{
self::assertSame(90.0, $this->calc->withDiscount(100.0, 10));
}
}
TDD формирует зависимости
Поскольку тест пишется первым, неудобные зависимости сразу становятся очевидными. Если класс сложно создать в тесте, это обратная связь о проектировании: внедряйте зависимости вместо того, чтобы создавать их внутри с помощью new. Приведённый ниже калькулятор скидок можно тестировать, передавая ему политику округления, а не задавая её жёстко, — TDD заставила выделить такую точку подмены.
<?php
interface RoundingPolicy { public function round(float $v): float; }
final class PriceCalculator
{
public function __construct(private RoundingPolicy $rounding) {}
public function withDiscount(float $amount, float $percent): float
{
return $this->rounding->round($amount - ($amount * $percent / 100));
}
}
// Tests inject a deterministic RoundingPolicy; no hidden global behavior.
Быстрая проверка
В чём смысл этапа «Красный»?
Итоги
Вы освоили TDD как дисциплину проектирования:
- Красный — зелёный — рефакторинг: неудачный тест, минимальный код, затем очистка при успешном прохождении.
- Триангуляция заставляет обобщать решение; новые требования появляются как новые неудачные тесты.
- Поставщики данных сокращают стабильные наборы случаев, а
--filter/--testdoxсохраняют цикл быстрым. - Рефакторьте и тесты: это код с долгим сроком жизни.
Далее: гибкие тестовые двойники с Mockery.
Часто задаваемые вопросы
Урок «Рабочий процесс разработки через тестирование» бесплатный?
Да — полный текст урока «Рабочий процесс разработки через тестирование» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Рабочий процесс разработки через тестирование»?
Формируйте дизайн с помощью цикла «красный — зелёный — рефакторинг» Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Рабочий процесс разработки через тестирование»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Рабочий процесс разработки через тестирование
- Мокирование и заглушки с Mockery
- Интеграционное и функциональное тестирование
- Мутационное тестирование с Infection