Работа начинается с тестирования

В жизни каждого разработчика наступает момент, когда он задумывается над созданием тестовой составляющей для своего детища. Поправлюсь — в жизни каждого хорошего разработчика. Когда ты джуниор и не несешь особой ответственности, имеешь право на уйму ошибок и можешь их исправить в любой момент. Ты не отвечаешь за тот продукт, что создаешь и не имеешь мотивации тратить лишнюю минуту на перепроверку созданного кода. «Да ничего, этот косяк не воспроизведется», «кажется, эта штука работает», «ну как минимум, она делает то что нужно» — если вы желаете перерасти уровень программерских яслей, то придётся свести на нет каждую из этих мыслей.

С развитием собственного опыта программирования, у вас появляются новые всё более и более крутые/крупные клиенты. От некоторых вы даже будете в восторге (от всех, если вы прям везунчик) — и люди хорошие, и оплачивают щедро, и не придирчивы к возникающим проблемам. Давайте рассмотрим один такой простой случай (очень простой, но главное то, что за этим стоит) создания обработчика формы от программиста, не знающего хлопот.

Итак, поступила простая задача — написать обработчик формы. Цель — принимать заявки от клиентов на покупку кирпичей. Заказчик крупный, занимается крупными поставками кирпичей оптом (допустим, на сумму от 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 тестах).

ок

Как видим, все тесты пройдены удачно и в базу внесена всего одна запись (так как только один метод был настроен на валидную работу).

результат внесён в базу

Каковы выводы? Разработка приложения, начиная с тестов, — более удачный вариант, чем обычное написание функционала. Необходимость получать от метода только то, что нужно, сравнимо с армейской дисциплиной — код делает ровно то, что ты от него требуешь, ни шага в сторону. Однако, у этого подхода есть и негативная сторона (хоть и спорная) — дело в том, что написание дополнительного функционала (которым является тестирование) также занимает часть времени, отведенного на разработку проекта. Как по мне, выбор однозначен — хороший программист должен писать тесты, и старт с тестового функционала помогает написать хорошо работающий и надежный проект. Попробую это использовать в чем-то менее тривиальном.

Комментарии 14

    +5

    Странно, что при этом ни разу не упоминается ни название, ни иные принципы TDD (именно так называется этот подход). Но статья мне понравилась, посыл верный

      0
      Однако, у этого подхода есть и негативная сторона (хоть и спорная) — дело в том, что написание дополнительного функционала (которым является тестирование) также занимает часть времени, отведенного на разработку проекта.

      Иногда функциональные тесты позволяют писать быстрее, т.к. для проверки не нужно проходить квест в интерфейсе (если она сложная).
      Например с behat, в типичных кейсах даже не требуется писать (PHP) код для тестов, достаточно написать сценарии, из готовых кирпичиков.
      (практически копипаст требований в gherkin формат)

        0
        Впредь, клиент сайта будет указывать только корректные данные, необходимые нам для дальнейшей обработки.


        Я правильно понял, что клиент просто не задал номер телефона? Но в таком случае тут ошибка как раз этих самых требований к клиенту, а не ошибка в программе. Если допускалось не вводить номер телефона, значит так и задумывалось изначально. Это уже потом поумнели и решили заставить заполнять это поле (правда, клиент всё равно может забить туда ерунду).
          0
          Попробую это использовать в чем-то менее тривиальном.


          Да, попробуйте обложить тестами, например, какую-либо игру. Желательно 3D (да хоть простейший Wolf-3D) — там много чего взаимосвязано от графики до физики и AI. Это не сарказм. Статьи на тему тестирования очень любят разбирать подобные вашим примеры. Но это сродни hello world. Какая в этом польза? Лучше бы показали полный цикл разработки одним человеком (потому что в статье речь идёт об одном человеке) большого приложения (тысяч на 30 строк на Си++ (без тестов)) с TDD и тестами. Вот это было бы полезно. Но почему-то таких статей никто писать не хочет. Ну и ещё, я как-то спрашивал, сильно ли тесты больше программы? Оказалось (на примере mySQL (или SQL lite — забыл)), раз в 10-20 больше, если покрывать всё тестами. То есть, вместо одной программы придётся писать 10. Написать ПО на 300 000-600 000 строк одному… Ну, я даже не знаю, как это сделать. В общем, именно потому, что ответов на эти вопросы я не знаю, тестирование в своих проектах (а я один их автор — я не в IT-конторе работаю) я сделать не могу (помилуйте, вот программа, в ней 68 000 строк, она часто меняется (понимаем лучше, что требуется для решения задачи аппаратуры) — я же к концу написания тестирования в психушку попаду!).
            0
            Не совсем то, что вы хотели, но и статья в хабах Laravel и PHP: Let's Build A Forum with Laravel and TDD laracasts.com/series/lets-build-a-forum-with-laravel
            демонстрируется полный цикл разработки с TDD одним человеком.

            тестирование в своих проектах (а я один их автор — я не в IT-конторе работаю) я сделать не могу (помилуйте, вот программа, в ней 68 000 строк, она часто меняется (понимаем лучше, что требуется для решения задачи аппаратуры) — я же к концу написания тестирования в психушку попаду!)

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

            А вообще, приверженцы TDD нередко утверждают, что разработка на самом деле идет быстрее, за счет уменьшения числа итераций рефакторинга (код сразу пишется тестируемым и подводные камни задачи всплывают раньше). Плюс, в целом от приверженцев тестирования, часто можно услышать, что времязатраты на написание и поддержку тестов компенсируются уменьшением времязатрат на проверку корректности кода и время затрат на его поддержку.
              0
              Это очень слабо связанные вещи.

              Тестирование, да, уменьшает затраты на последующую поддержку, причём в разы. Но из этого самого по себе никак не следует именно TDD.

              Ситуация, что test-first из TDD приводит к «код сразу пишется тестируемым», а без него — не приводится, возникает только при условии, что кодер иначе бы поленился правильно писать, а никто сверху бы его не заставил. Поэтому TDD хорош как средство форсировать тестирование в условиях ленивых задниц на всех уровнях, кроме верхнего — его нарушение превращается в административный проступок, более серьёзный, чем просто недотестирование. В прочих же условиях test-first приводит к тому, что явно запрещается возможность качественно подумать о реализации и испытать её там, где заранее неизвестен путь реализации.

              Концентрация на test-first приводит к игнорированию возможности, что какой-то тест может незаметно сломаться уже после его первичного удовлетворения.

              Самое смешное, что формально в TDD ничего не говорится про удовлетворение поставленной задачи для кода, а лишь про удовлетворение сформированных для неё тестов :))

              По сумме, TDD это технология для быстрого написания одноразового несложного кода без расчёта на длительное развитие и поддержку.

              Если нет проблем с административным обеспечением и цейтнота, запрещающего тестирование, то вместо TDD надо смотреть на гарантированное обеспечение acceptance-критериев на всех уровнях при обязательном ревью кода на предмет выполнения поставленных целей, и отдавать достаточное время отработке краевых случаев.
                0
                Я не утверждал, что всё так и есть, мне не довелось испытать TDD, но в материалах по этому подходу часты утверждения, что TDD это не «напишите тест, подгоните под него код, профит», а что сам подход позволяет в итоге решать задачу более верным способом за то же время или даже быстрее. Например, за счет отсрочки момента написания результирующего кода. Предполагается, что взгляд на код с точки зрения теста улучшает понимание проблемы и будущей реализации ее решения.
                  0
                  > а что сам подход позволяет в итоге решать задачу более верным способом за то же время или даже быстрее.

                  Есть такой дисциплинирующий фактор. Но,
                  1) Существенно это в первую очередь для джунов, которые иначе слишком склонны к наворачиванию неуправляемых простыней только потому, что так привыкли; тот, кто хотя бы пару раз обжёгся на том, что не понимает, как свой код тестировать, уже будет стараться так не делать. В оригинальном представлении TDD это различие никак не отражается, всех загоняют в одно прокрустово ложе.
                  2) Отсюда, это свойство test-first, но не TDD в целом. Что означает, что применить test-first в конкретном случае типа «я не понимаю, что тут писать, но уже есть идея, как его тестировать», можно и нужно. Но обобщать на все случаи, мягко говоря, нежелательно.
                  Без возможности продумать в целом, а потом только задуматься о тестировании, не возник бы ни один из тех алгоритмов, которыми так гордится отрасль — начиная с великих сортировок.

                  > Предполагается, что взгляд на код с точки зрения теста улучшает понимание проблемы и будущей реализации ее решения.

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


                Их неоткуда взять. Увы.

                А вообще, приверженцы TDD нередко утверждают, что разработка на самом деле идет быстрее


                У меня такой фокус ни разу не получился.
                0
                На моей практике обычное соотношение строк кода к тестам это от 1:1 до 1:3. Базы данных — это отдельный случай, от которых требуется повышенная надёжность на уровне 146%. Тоже самое касается например и космической, авиационной, медицинской техники, где каждые 10 строчек кода по трудозатратам выливаются в эквивалент 100-1000 строк типичного веб приложения. Что касается игр, физика, графика — это движок, по отдельности они прекрасно тестируются я не поверю, что промышленные движки вроде Unreal Engine не используют тесты.

                Если у вас есть программа на 68_000 строк кода, которая постоянно меняется и без какого либо покрытия тестами — это и есть прямой путь в психушку.
                  0
                  На моей практике обычное соотношение строк кода к тестам это от 1:1 до 1:3.


                  Это при каком покрытии? 100%?

                  где каждые 10 строчек кода по трудозатратам выливаются в эквивалент 100-1000 строк типичного веб приложения.


                  Я как раз и для военной и для космической делаю. Нет там такого.

                  я не поверю, что промышленные движки вроде Unreal Engine не используют тесты.


                  У них людей много. Попробуйте сделать и тесты и движок самостоятельно в одиночку.

                  Если у вас есть программа на 68_000 строк кода, которая постоянно меняется и без какого либо покрытия тестами — это и есть прямой путь в психушку.


                  Если структура достаточно прозрачна и отработана, то управлять такой программой достаточно просто. А вот постоянно менять тесты на неё — не очень просто.
                    –1
                    Это при каком покрытии? 100%?

                    Формальное покрытие на уровне 90%-100%, фактическое будет ниже.

                    Я как раз и для военной и для космической делаю. Нет там такого.

                    Вы в одиночку поддерживаете какой то код для военки и космоса? Я могу судить про эту отрасль только из тех же публикаций на хабре, где не однократно отмечалось, что любое изменение кода — это множественные проверки и сертификации.

                    Если структура достаточно прозрачна и отработана, то управлять такой программой достаточно просто. А вот постоянно менять тесты на неё — не очень просто.

                    Есть отличный маркер плохо написанных тестов — когда одна строчка изменения в рабочем коде каскадно вызывает 100 (условно) правок в тестах. Хорошо изолированный код тестов менять не труднее, чем сам код, при этом есть гарантия, что у вас ничего не отвалится в других местах. Нужно точно также учиться грамотно писать тесты, как и учиться писать грамотный рабочий код. Пожалуй это вполне могут два разных независимых навыка которые прокачиваются с опытом.
                      0
                      Вы в одиночку поддерживаете какой то код для военки и космоса?

                      Увы, да.

                      Я могу судить про эту отрасль только из тех же публикаций на хабре, где не однократно отмечалось, что любое изменение кода — это множественные проверки и сертификации.


                      Может, где-то так и есть, но я такого не встречал. Моя практика показывает, что проблема в 99% случаях не в функциях модуля (что и проверяет тест). А вот в чём: представьте, что у вас код отработки чрезвычайной ситуации того же Шаттла. Тестами вы только лишь убедитесь, что ваши функции работают так, как вы задумали. Практика же внесёт замечательные коррективы, когда обнаружится, что реакция должна быть несколько иной, и ошибка на самом деле в идеологии действий, а не в реализации этих тривиальный действий. Тут модульные тесты не помогут. А тестировать эти самые тривиальные действия в целом бессмысленно — ошибка в них проявляется практически сразу же после запуска и после исправления никогда не возникает вновь. Тогда что дадут имеющиеся тесты? Правильно, ничего нового. Интеграционное тестирование даст, но сейчас речь не о нём.

                      Хорошо изолированный код тестов менять не труднее, чем сам код


                      Дело в другом — перестроив программу, придётся переделывать и тесты. Эдакая двойная работа. Вам придётся переписывать старые моки и стабы, реализовывать новые. Одному всё это делать тяжко.
                0

                А может просто дописать одну строку кода, для ведения лога запросов с исходными данными?

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое