0Pricing
PHP Academy · Урок

Рабочий процесс разработки через тестирование

Формируйте дизайн с помощью цикла «красный — зелёный — рефакторинг»

«Рабочий процесс разработки через тестирование» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Рабочий процесс разработки через тестирование
  2. Мокирование и заглушки с Mockery
  3. Интеграционное и функциональное тестирование
  4. Мутационное тестирование с Infection
← Назад к PHP Academy