Коротко: Pest PHP позволяет тестировать структуру вашего кода, а не только его поведение. Опишите правила вашей команды в виде архитектурных тестов, а CI будет проверять их при каждом коммите. Один такой тест помог предотвратить утечку данных между клиентами, которую пропустил человек при код-ревью.
У нас было правило. Каждая модель, содержащая данные конкретного клиента, должна использовать наш трейт BelongsToTenant. Этот трейт добавляет глобальный скоуп, который не позволяет одной клинике видеть данные другой клиники.
Правило входило в программу онбординга. Оно было в чек-листе для код-ревью. О нем знали все.
В команду пришел новый разработчик. Спустя три недели он добавил новую модель и забыл про трейт. Ревьюер был сосредоточен на бизнес-логике, которая была написана действительно хорошо, и не заметил отсутствия трейта. Модель ушла в продакшн.
В течение двух дней одна клиника могла видеть фрагменты данных другой клиники в одном конкретном отчете. Утечку обнаружили через тикет в поддержку. Наши тесты ее не заметили.
В тот день в проекте появились архитектурные тесты.
Что такое архитектурный тест
Большинство тестов проверяют поведение. Передаем такие входные данные — функция возвращает такой результат. Архитектурный тест проверяет структуру. Он утверждает что-то о том, как организован код, а не о том, что он вычисляет.
В Pest для этого предусмотрена специальная функция arch.
// tests/Architecture/ArchTest.php
arch('tenant models must use the BelongsToTenant trait')
->expect('App\Models')
->toUseTrait('App\Traits\BelongsToTenant')
->ignoring('App\Models\SystemSetting');
arch('controllers may not touch the DB facade directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB');
arch('services may not depend on the HTTP request')
->expect('App\Services')
->not->toUse('Illuminate\Http\Request');
arch('no env calls outside config files')
->expect('App')
->not->toUse('env');
Они запускаются в CI при каждом коммите. Нарушаете правило — и сборка падает с сообщением, в котором указано название правила и нарушивший его файл.
Тесты, которые полностью себя оправдали
В течение следующих месяцев тест на трейт мультиарендности выявил еще четыре модели, в которых этот трейт отсутствовал. Каждая из них представляла потенциальную утечку данных. Один этот тест окупил все затраченные усилия.
Тест контроллеров пресекает подход «я тут быстренько добавлю один запрос». Это случается чаще, чем принято признавать, обычно в условиях дедлайнов.
// This fails the architecture test
class ReportController extends Controller
{
public function index()
{
$total = DB::table('orders')->sum('total');
return response()->json($total);
}
}
Тест для env — это то, чего, как мне жаль, мы не добавили с первого дня. Вызовы env напрямую вне файлов конфигурации работают в разработке и возвращают null в продакшне после кеширования конфигурации. Это классическая ловушка Laravel. Тест делает повторное попадание в нее невозможным.
Внедрение в существующую кодовую базу
Если вы добавите строгие архитектурные тесты в большой старый проект, вы получите сотни ошибок одновременно. Команда встретит это в штыки, и процесс забуксует. Вот схема внедрения, которая реально сработала у нас.
Начните с одного правила. Самого важного. Для нас это был трейт мультиарендности. Исправьте несколько найденных нарушений и замержьте только их.
После этого добавляйте по одному правилу в неделю. Достаточно медленно, чтобы команда успевала принять каждое из них и исправить обнаруженные старые нарушения.
Для правила с большим количеством унаследованных нарушений используйте метод ignoring, чтобы исключить старый код.
arch('controllers may not use the DB facade')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB')
->ignoring('App\Http\Controllers\Legacy');
Это обеспечивает соблюдение правил для всего нового кода, пока вы рефакторите старый код в своем темпе. Затем отслеживайте количество исключений в ignoring. Это число должно уменьшаться с каждым спринтом. Если оно растет, значит, новый код пишется в обход ограничений для старого кода, и у вас проблемы.
Моя ошибка
Я попытался добавить десять правил в одном пулл-реквесте. Сборка покраснела от ошибок во всей кодовой базе. Это было чересчур, и команда оказала жесткое сопротивление. Я чуть было не бросил эту затею.
Добавление правил по одному — это не приятная мелочь. Это единственный способ заставить это работать в реальном проекте. Стена из красных упавших тестов заставляет людей хотеть удалить тесты. Одно новое упавшее правило заставляет исправить конкретную вещь.
Что бы я сделал по-другому
Я бы добавлял архитектурные тесты с самого первого коммита в каждом новом проекте. Добавление их в существующий проект — это всегда догоняющая работа. Начиная с чистого листа, вы гарантируете, что правила соблюдаются с самого начала и никогда не возникает накопленного долга по нарушениям.
Я бы также создал небольшой общий пакет с набором типовых тестов. Мы писали практически идентичные архитектурные тесты в трех проектах. Переиспользуемый пакет помог бы избежать этого дублирования.
Мое честное мнение
Архитектурные тесты нужны не для того, чтобы быть строгими ради самих себя. Они нужны для того, чтобы сделать кодовую базу предсказуемой.
Когда у каждой клиентской модели гарантированно есть трейт, изоляция — это не надежда. Это факт, проверяемый сборкой. Когда ни один контроллер не может обратиться к базе данных, сервисный слой действительно становится единственным путем. Эти гарантии сохраняются по мере изменения команды и роста кодовой базы, потому что машина проверяет их на каждом коммите и человеку не нужно ничего держать в голове.
Правила вашей команды должны находиться в наборе тестов. Правило в вики — это просто пожелание. Правило в CI — это реальность.
Какое правило вы бы превратили в тест в первую очередь, если бы могли?
Комментарии (0)
Пока нет комментариев — будьте первым.