Тесты и Архитектура: как заставить код рассказывать о себе

«Архитектура без тестов — как дом без чертежей: стоит, пока не подует ветер»

Вы открываете репозиторий. 487 файлов. README.md последний раз редактировался в 2019 году. В комментариях к коду: // TODO: fix later (c) 2021. Вы делаете глоток **тут-могла-быть-ваша-реклама** и задаёте себе вопрос, который мучает разработчиков со времён Форта: «А оно вообще понимает, что делает?»

Знакомо? Тогда у меня для вас две новости. Хорошая: тесты могут стать вашим компасом в этом лабиринте. Плохая: если тестов нет, архитектура проекта, скорее всего, держится на честном слове и try-catch в самом низу стека.

Давайте разберёмся, как тесты, архитектура и TDD переплетаются, почему современные инструменты вроде Pest и Mockery перестали быть «болью для джуниоров», и как начать писать тесты, не превращая рабочий день в марафон выживания.

Тесты как рентген архитектуры

Зачем вообще нужны тесты? Если отвечать честно: чтобы вы могли спать ночью, не просыпаясь от звонка в Slack с заголовком PROD DOWN.

Но если копнуть глубже, тесты — это живая документация архитектуры. Архитектура в голове архитектора умирает вместе с ним. Архитектура в виде тестов — остаётся и проверяется при каждом git push.

Можно ли через тесты понять архитектуру проекта?

Да, и даже нужно. Структура тестов почти всегда зеркалит структуру системы:

  • Feature/ или Browser/ тесты показывают, как пользователь взаимодействует с системой (сценарии, маршруты, бизнес-процессы).
  • Unit/ тесты раскрывают компоненты, их границы и зависимости.
  • Если тесту для проверки одного метода нужно мокать 12 сервисов, создавать 3 БД-транзакции и молиться на sleep(2) — поздравляю, вы нашли "плотно соединенные спагетти". Хорошая архитектура тестируется легко, потому что она слабо связана.
Запомните: Архитектура без тестов — как дом без чертежей: стоит, пока не подует ветер. А тесты — это те самые чертежи, которые ещё и автоматически проверяют, не провалился ли пол под диваном.

TDD: трата времени или невидимый архитектор?

«TDD — это писать тесты до кода. Значит, я трачу в 2 раза больше времени на фичу». Это самый устойчивый миф в индустрии, рядом с «я исправлю это потом» и «оно же работает, не трогай».

TDD — это не про тесты. Это про дизайн.

Когда вы пишете тест первым, вы вынуждены задать вопрос: «Как я ХОЧУ использовать этот класс?» Вы начинаете с интерфейса, а не с реализации. Это автоматически приводит к:

  • Чётким границам ответственности (SRP сам приходит в гости).
  • Внедрению зависимостей вместо new Service() внутри другого сервиса.
  • Отсутствию побочных эффектов и глобального состояния.

Минимальный пример цикла TDD на Pest:

// tests/Feature/OrderTest.php
it('calculates total with discount', function () {
    $order = new Order();
    $order->addItem(price: 1000, quantity: 2);
    $order->applyDiscount(10);

    expect($order->total())->toBe(1800);
});

Сначала тест падает (Class 'Order' not found). Вы создаёте класс, метод, заглушку. Запускаете — зелёный. Рефакторите. И только потом думаете, как это всё сохранить в БД.

TDD — это не про то, чтобы писать тесты. Это про то, чтобы писать код, который не хочется выкидывать в окно в 3 часа ночи.

Тратите ли вы время в начале? Да. Экономите ли вы часы/недели на отладке, рефакторинге и onboarding новых разработчиков позже? «Абсолютли!» (с). TDD — это инвестиция в архитектурную ликвидность проекта.

Легаси без тестов: как не сгореть на работе

«У нас нет тестов. Система работает 5 лет. Мы боимся трогать код». Классика. Как начать рефакторинг, когда архитектура похожа на клубок наушников в кармане джинсов?

Ответ: характеризационные тесты (characterization tests).

Вы не знаете, как система должна работать. Но вы точно знаете, как она работает сейчас. Пишите тесты, которые фиксируют текущее поведение, даже если оно выглядит странным. Это ваша страховка перед рефакторингом.

Практический алгоритм:

  1. Найдите «горячую точку» — кусок кода, который чаще всего меняют или который падает.
  2. Оберните вызовы внешних зависимостей в интерфейсы.
  3. Напишите тест, который проверяет вход → выход для текущего кода.
  4. Меняйте внутренности. Если тест зелёный — вы ничего не сломали.
  5. Постепенно заменяйте new ConcreteDependency() на инъекции, улучшая архитектуру шаг за шагом.
Легаси без тестов — как коробка с пауками: открывать страшно, но если надеть перчатки (тесты) и действовать по одному, можно аккуратно рассортировать всё по полочкам.

Не пытайтесь покрыть 100% за спринт. Начните с одного сценария, который приносит бизнесу деньги. Остальное догонит.

Pest и Mockery: почему новичкам стало легче дышать

Раньше написание тестов в PHP напоминало заполнение налоговой декларации: много шаблонов, строгие классы-наследники, setUp(), tearDown(), @dataProvider и вечный поиск, где же упала assertion.

Пришли Pest и Mockery. И да, они реально помогают новичкам. И старичкам тоже.

Почему Pest проще?

  • Меньше кода, больше смысла. test() / it() вместо создания классов.
  • Встроенные матчеры, цепочки, читаемые сообщения об ошибках.
  • Работает поверх PHPUnit, так что экосистема не ломается.

Почему Mockery спасает?

Тестировать код, который ходит в платёжку, шлёт SMS или читает погоду из API — это ад, если не мокать. Mockery позволяет заменить реальную зависимость на «камуфляжного каскадёра», который отдаёт то, что вы скажете.

Минимальная связка Pest + Mockery:

// tests/Unit/NotificationServiceTest.php
use App\Services\NotificationService;
use Mockery;

afterEach(function () {
    Mockery::close(); // Обязательно!
});

it('sends email when order is created', function () {
    $api = Mockery::mock(\App\Api\EmailApi::class);
    $api->shouldReceive('send')
        ->with('user@example.com', 'Order #123 created')
        ->once()
        ->andReturn(true);

    $service = new NotificationService($api);
    $service->notifyOrderCreated('user@example.com', 123);

    expect(true)->toBeTrue(); // Или просто не будет ошибок, если моки не сработали
});

Вам не нужно писать 30 строк для TestCase. Не нужно бороться с PHPUnit-датапровайдерами. Вы описываете что должно произойти, а не как настроить фреймворк.

Mockery — это ваш личный каскадёр. Он получает удары вместо реального внешнего сервиса, а вы спокойно проверяете, правильно ли ваш герой уворачивается.

Для новичков это снимает когнитивный барьер: меньше магии, больше фокуса на предметной области.

Как начать без паники: 5 шагов

Если вы до сих пор откладывали тесты «на потом», вот дорожная карта без героизма и переработок:

  1. Выберите одну маленькую, стабильную фичу. Не лезьте в ядро платёжки или авторизацию. Возьмите расчёт скидки, форматирование даты или валидацию поля.
  2. Установите Pest. composer require pestphp/pest --dev. Запустите ./vendor/bin/pest --init. Готово.
  3. Напишите один падающий тест. Ожидаемый результат → реальный вызов → expect()->toBe(). Запустите. Увидьте красный. Порадуйтесь.
  4. Добавьте Mockery для внешних зависимостей. Не ходите в БД, не стучитесь в API. Изолируйте единицу.
  5. Сделайте тест зелёным. Затем отрефакторите. Если после рефакторинга тест всё ещё зелёный — вы молодцы. Если нет — вы нашли скрытый баг. Тесты только что отработали.

Золотые правила старта:

  • Тестируйте поведение, а не реализацию. expect($result)->toBe(1500) лучше, чем expect($obj->privateCalculation())->toBeCalled().
  • Начинайте с feature/integration тестов, если unit пугает. Они ближе к бизнесу и проще в написании.
  • Не гонитесь за покрытием в %. 30% критического кода > 90% геттеров/сеттеров.
Не пытайтесь покрыть весь проект за вечер. Даже супергерои начинают с плаща, а не сразу летают.

Заключение

Тесты и архитектура — это не два разных мира. Это две стороны одной монеты. Тесты заставляют архитектуру быть чистой, модульной и предсказуемой. Архитектура делает тесты простыми и быстрыми.

TDD не тратит ваше время. Он инвестирует его в то, чтобы ваш код пережил смену фреймворков, уход ключевых разработчиков и внезапное «а давайте добавим мультивалютность». А Pest и Mockery просто убирают трение, превращая написание тестов из обязаловки в естественную часть разработки.

Начните с одного теста сегодня. Завтра он спасёт вам вечер. Через месяц — он спасёт команду от деплоя в пятницу. А через год вы с улыбкой будете открывать репозиторий, где composer test зелёный, а архитектура читается как хорошая книга.

И да, живите спокойно и занимайтесь полезными для себя делами. Теперь вы можете себе это позволить.

🧪 Тестируйте сегодня — спите спокойно завтра.
Потому что хороший код заслуживает хорошей страховки.
P.S. Если ваш коллега говорит «тесты не нужны, я и так помню, как это работает», покажите ему эту статью. Или просто молча добавьте expect($hisCode->work())->toBe(false); в свой следующий PR. Шутка. (Или нет.)

На связи

Нашли неточность? Есть вопрос по тестам? Хотите поделиться опытом?

Давайте вместе делать Мир лучше!

Обсуждение

💬 Есть вопросы? Пишите в Telegram-канал или на ping@ambrion.dev