В жизни каждого разработчика наступает момент, когда он задумывается над созданием тестовой составляющей для своего детища. Поправлюсь — в жизни каждого хорошего разработчика. Когда ты джуниор и не несешь особой ответственности, имеешь право на уйму ошибок и можешь их исправить в любой момент. Ты не отвечаешь за тот продукт, что создаешь и не имеешь мотивации тратить лишнюю минуту на перепроверку созданного кода. «Да ничего, этот косяк не воспроизведется», «кажется, эта штука работает», «ну как минимум, она делает то что нужно» — если вы желаете перерасти уровень программерских яслей, то придётся свести на нет каждую из этих мыслей.
С развитием собственного опыта программирования, у вас появляются новые всё более и более крутые/крупные клиенты. От некоторых вы даже будете в восторге (от всех, если вы прям везунчик) — и люди хорошие, и оплачивают щедро, и не придирчивы к возникающим проблемам. Давайте рассмотрим один такой простой случай (очень простой, но главное то, что за этим стоит) создания обработчика формы от программиста, не знающего хлопот.
Итак, поступила простая задача — написать обработчик формы. Цель — принимать заявки от клиентов на покупку кирпичей. Заказчик крупный, занимается крупными поставками кирпичей оптом (допустим, на сумму от 500 000 рублей — для того чтобы почувствовать хоть какой-то уровень ответственности за происходящее). Конкуренция бешеная — клиенты могут быстро перейти к поставщику кирпичей, если не ответить в течение суток.
Нашему программисту сказали, что нужно сохранять из формы данные клиента — ФИО представителя, номер телефона, название предприятия клиента, объем заказа и необязательное поле описания заказа. Пораскинув мозгами, быстро была создана простейшая форма со стандартными полями для лицевой части сайта:
Данные с формы отправляются AJAX-запросом, без перезагрузки страницы. Далее, программист берется за оформление обработчика формы и ему нужно справиться с довольно тривиальной задачей — добавить в уже существующую таблицу Orders записи по новому клиенту и отправить заказчику письмо на почту с оповещением по новому клиенту.
Форма работает, данные успешно сохраняются, заказчик доволен. Но вдруг от заказчика поступает гневный звонок “так мол и так, заказ поступил — компания-миллионер хочет купить у меня все кирпичи, но с формы не пришел номер телефона и как теперь с ними связаться?! Завтра они найдут другого поставщика! Как так, что ты сделал?! По твоей вине...”. Заказчик рвёт и мечет, минус нервы, минус доверие и минус уважение. Ситуация крайне стандартная для джуниора — отсутствие какой-либо валидации и тестирования входящих данных с формы. Первая задача (валидация) решается крайне просто, через добавление правил валидации:
Впредь, клиент сайта будет указывать только корректные данные, необходимые нам для дальнейшей обработки. На этом же этапе разработчика посещает мысль о необходимости тестирования кода для дальнейшего избежания столь неловкой ситуации. К примеру, тестирование поля фамилии будет иметь следующий вид (для упрощения базового примера, csrf защита отключена):
Мы знаем, что при отсутствии данного поля код должен вернуть ответ с ошибкой и прописанным нами статусом 400. Такие методы тестирования прописываются для каждой конкретной ситуации (или конкретной валидации поля, здесь уж всё зависит от поставленных задач и фантазии разработчика).
Но есть ли другой способ разработки, отличный от “я сделал, а теперь проверю”? Мы сначала пишем код, натыкаемся на косяки исполнения, исправляем, а потом вспоминаем про тесты. Данный подход может выйти для нас и нашего заказчика боком, учитывая потерянного многомиллионного клиента (хоть и теоретически, но всем бы таких клиентов). И тут я задался вопросом — а что если мы логику создания приложения начнем с обратного конца — сначала предъявим требования к “исполнителю”, а затем заставим его этим требованиям соответствовать? Давайте попробуем.
Задачу оставим прежнюю, поменяем лишь подход к ней. Нам нужно написать обработчик формы с полями fio, phone, corp, quant и content. Результат успешного выполнения — статус 200, добавление поля в Order с сообщением “ok” и возврат данных по внесенной записи, остальные варианты — статус 400 и список ошибок.
Первым делом нам необходимо написать метод тестирования валидного заполнения данных формы:
Далее, создаем необходимый роут и метод контроллера (пока пустой). Если запустим проверку сейчас, то вполне ожидаемо получим ошибку. Проверка валидного заполнения данных — это не всё, что нам нужно. Теперь приступаем к тестированию валидации полей формы. Определяем, какие поля обязательны — fio, phone, corp, quant и добавляем метод на проверку (comment не является обязательным):
Обработчик формы просто обязан будет проверить на наличие входящие данные fio, phone, corp, quant. Поскольку мы убрали все обязательные поля из запроса, то по каждому из них должна вернуться ошибка в errors. В случае, если хотя бы одного из них нет — проблема исполнения. При желании, можно добавить проверку на message, как было сделано ранее (проверка на “ok”).
Оформляем проверку на минимальную длину полей fio, phone и corp (аналогично будет сделана проверка на максимальную длину и на недопустимые символы в этих полях).
Наши проверки оформлены, можно запускать и проверять
Идеально. Наше приложение крашнулось в 5 из 5 тестов. Дальнейшая наша цель — пройтись по тестовым методам, устанавливающим invalid значения в поля, и сформировать правила валидации на входящие данные. Логика примерно такая: поле fio не может быть пустым; длина не менее 3 и не более 120; это строка с набором символов, допускаемых в имени (буквы, дефис, отступ). Результат такой логики по всем полям:
В ответ в случае фейла добавлен список ошибок errors, которые соответствуют каждому «проблемному» полю. Это нам поможет проверять конкретные поля на валидацию (assertJsonStructure в файле теста). Далее, дописываем метод под валидную проверку и получаем итоговый вариант:
И наконец-то мы можем проверить, как наш скрипт отрабатывает тестирование (напомню, было 5 фейлов в 5 тестах).
Как видим, все тесты пройдены удачно и в базу внесена всего одна запись (так как только один метод был настроен на валидную работу).
Каковы выводы? Разработка приложения, начиная с тестов, — более удачный вариант, чем обычное написание функционала. Необходимость получать от метода только то, что нужно, сравнимо с армейской дисциплиной — код делает ровно то, что ты от него требуешь, ни шага в сторону. Однако, у этого подхода есть и негативная сторона (хоть и спорная) — дело в том, что написание дополнительного функционала (которым является тестирование) также занимает часть времени, отведенного на разработку проекта. Как по мне, выбор однозначен — хороший программист должен писать тесты, и старт с тестового функционала помогает написать хорошо работающий и надежный проект. Попробую это использовать в чем-то менее тривиальном.
Автор: Виталик Иващенко