Тесты как рентген архитектуры
Зачем вообще нужны тесты? Если отвечать честно: чтобы вы могли спать ночью, не просыпаясь от звонка в 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). Вы создаёте класс, метод, заглушку. Запускаете — зелёный. Рефакторите. И только потом думаете, как это всё сохранить в БД.
Тратите ли вы время в начале? Да. Экономите ли вы часы/недели на отладке, рефакторинге и onboarding новых разработчиков позже? «Абсолютли!» (с). TDD — это инвестиция в архитектурную ликвидность проекта.
Легаси без тестов: как не сгореть на работе
«У нас нет тестов. Система работает 5 лет. Мы боимся трогать код». Классика. Как начать рефакторинг, когда архитектура похожа на клубок наушников в кармане джинсов?
Ответ: характеризационные тесты (characterization tests).
Вы не знаете, как система должна работать. Но вы точно знаете, как она работает сейчас. Пишите тесты, которые фиксируют текущее поведение, даже если оно выглядит странным. Это ваша страховка перед рефакторингом.
Практический алгоритм:
- Найдите «горячую точку» — кусок кода, который чаще всего меняют или который падает.
- Оберните вызовы внешних зависимостей в интерфейсы.
- Напишите тест, который проверяет вход → выход для текущего кода.
- Меняйте внутренности. Если тест зелёный — вы ничего не сломали.
- Постепенно заменяйте
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-датапровайдерами. Вы описываете что должно произойти, а не как настроить фреймворк.
Для новичков это снимает когнитивный барьер: меньше магии, больше фокуса на предметной области.
Как начать без паники: 5 шагов
Если вы до сих пор откладывали тесты «на потом», вот дорожная карта без героизма и переработок:
- Выберите одну маленькую, стабильную фичу. Не лезьте в ядро платёжки или авторизацию. Возьмите расчёт скидки, форматирование даты или валидацию поля.
- Установите Pest.
composer require pestphp/pest --dev. Запустите./vendor/bin/pest --init. Готово. - Напишите один падающий тест. Ожидаемый результат → реальный вызов →
expect()->toBe(). Запустите. Увидьте красный. Порадуйтесь. - Добавьте Mockery для внешних зависимостей. Не ходите в БД, не стучитесь в API. Изолируйте единицу.
- Сделайте тест зелёным. Затем отрефакторите. Если после рефакторинга тест всё ещё зелёный — вы молодцы. Если нет — вы нашли скрытый баг. Тесты только что отработали.
Золотые правила старта:
- Тестируйте поведение, а не реализацию.
expect($result)->toBe(1500)лучше, чемexpect($obj->privateCalculation())->toBeCalled(). - Начинайте с feature/integration тестов, если unit пугает. Они ближе к бизнесу и проще в написании.
- Не гонитесь за покрытием в %. 30% критического кода > 90% геттеров/сеттеров.
Заключение
Тесты и архитектура — это не два разных мира. Это две стороны одной монеты. Тесты заставляют архитектуру быть чистой, модульной и предсказуемой. Архитектура делает тесты простыми и быстрыми.
TDD не тратит ваше время. Он инвестирует его в то, чтобы ваш код пережил смену фреймворков, уход ключевых разработчиков и внезапное «а давайте добавим мультивалютность». А Pest и Mockery просто убирают трение, превращая написание тестов из обязаловки в естественную часть разработки.
Начните с одного теста сегодня. Завтра он спасёт вам вечер. Через месяц — он спасёт команду от деплоя в пятницу. А через год вы с улыбкой будете открывать репозиторий, где composer test зелёный, а архитектура читается как хорошая книга.
И да, живите спокойно и занимайтесь полезными для себя делами. Теперь вы можете себе это позволить.
Потому что хороший код заслуживает хорошей страховки.
expect($hisCode->work())->toBe(false); в свой следующий PR. Шутка. (Или нет.)
На связи
Нашли неточность? Есть вопрос по тестам? Хотите поделиться опытом?
- Telegram-канал
- ping@ambrion.dev
- GitHub — для идей и дискуссий
Давайте вместе делать Мир лучше!