тестирование
#программирование,  #разоблачения

Все-таки вам нужны юнит-тесты

В прошлом посте я говорил, что вам не нужны юнит-тесты. Но то были #вредныесоветы, а сейчас настало время разоблачений. Сегодня мы поговорим о всей важности тестирования и о том, чем грозит его отсутствие (либо неправильное применение). Я постараюсь максимально подробно описать плюсы тестирования и разобрать вредные советы из прошлого поста.

В качестве вступления

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

Но я занимаюсь enterprise разработкой. Я знаю, как писать бэкенд для крупных сервисов и, что самое главное – я знаю, как применять модульное и интеграционное тестирование в больших проектах на языках высокого уровня.

Спойлер
А может и не знаю 🙂 В любом случае, мне есть, чем поделиться.

Я занимаюсь проектами, в которых минимум пользовательского интерфейса, а многие (микро)сервисы и в принципе его не имеют. Проще говоря, существует единая точка входа в процесс, но до визуального отображения результатов работы будет вызвано множество функций в разных частях системы. Все, что они делают – обрабатывают, подготавливают, сохраняют и пересылают данные из одних мест в другие. И я хочу рассказать, насколько важно модульное и интеграционное тестирование в таких системах.

Простейший пример приложения

Чтобы лучше понять, как это – писать систему без интерфейса, давайте рассмотрим простейший пример. Допустим, у вас есть telegram-бот, который складывает числа. Он просто суммирует их и не делает ничего больше.

Вы ему присылаете несколько чисел, разделенных пробелом, например: «2 2 5». Он эти числа парсит, складывает между собой и возвращает вам результат: 9.

Выглядит очень просто, но это – приложение без какого-либо интерфейса. Да, здесь интерфейсом выступает сам telegram, но точкой входа в вашу систему будет являться вебхук-контроллер, который принимает события от telegram. Иными словами, вы сами никакого пользовательского интерфейса не предоставляете.

На бумаге это будет выглядеть как-то так:

  1. Пользователь отправляет вам сообщение, которое уходит на сервер telegram. 
  2. Telegram оповещает ваше приложение о новом сообщении с помощью вебхуков.
  3. Ваше приложение выполняет необходимые действия и отправляет на сервер telegram ответ.
  4. Telegram оповещает пользователя о новом сообщении.

Диаграмма довольно условная, и telegram может работать по-другому, но в качестве примера сгодится вполне.

Ручного тестирования недостаточно

Вы можете запустить ваше приложение локально, настроить вебхуки и начать тестировать, используя клиент telegram. Иными словами, заняться ручным тестированием. И вы не будете испытывать каких-либо неудобств.

А теперь представьте, что система разрастается. По какой-то неведомой нам причине миллионы пользователей заинтересовались вашим приложением и бесконечно шлют свои сообщения. Вы решаете разделить ваше приложение на consumer, processor и producer:

  1. Consumer принимает вебхуки и складывает сообщения в очередь входящих сообщений.
  2. Processor забирает сообщения из очереди входящих сообщений, складывает числа и кладет результат в другую очередь.
  3. Producer берет сообщения из очереди результатов и отправляет их пользователям.

В таком контексте вам нужно будет поднять три локальных сервиса, две очереди сообщений и настроить связь между ними. Вы станете это делать лишь для того, чтобы проверить, что «2+2» вернет 4, а «3+привет» вернет «Извините, но вы должны ввести числа» (а не «3привет», как сами знаете где)?

Личное мнение – мануальное тестирование умрет
Лично я считаю, что понятие «ручное тестирование» очень сильно изменится, а мануальные тестировщики попросту перестанут существовать. Тестирование веб-приложений отлично автоматизируется тем же Selenium (либо чем-то еще), тестирование десктопных и мобильных приложений тоже будет автоматизироваться. Нет, ручное тестирование само по себе никуда не денется, просто тестировщики будут проводить целый комплекс работ – от создания и поддержки автотестов до эпизодического тестирования руками там, где это проще. Чисто ручное тестирование без использования скриптов там, где это возможно – это рудимент.
Здесь начинается тестирование

Да, разумеется, когда сервис отправится в тестирование (в QA отдел), самим тестировщикам не придется множество раз и подолгу его настраивать. По-хорошему, за них это должны делать скрипты, те самые пайплайны, над которыми мы смеялись в прошлый раз.

Да даже если тестировщику и придется все это поднимать, то это же часть его работы, верно? Ну поднимет и начнет тестировать, вроде бы никаких проблем.

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

И, чтобы разработчику не пришлось каждый раз поднимать весь контекст у себя на машине, и были придуманы юнит- и интеграционные тесты. Вы просто берете и тестируете отдельно модуль сложения чисел, модуль получения входных данных из очереди входящих сообщений и модуль отправки результатов в выходную очередь. Когда вы делаете какое-то изменение, вы запускаете эти тесты и смотрите, что у вас все в порядке, и затем уже отдаете результат в отдел QA для проведения более объемлющего тестирования.

Далее под словом «тестирование» я буду подразумевать не работу отдела QA, а написание разработчиком юнит- и интеграционных тестов на свой код.

Далее, пробежимся по вредным советам из прошлого поста.

Хороший программист уверен в себе

Помните шутку про стадии роста программиста?

  1. Джуниор: я ничего не знаю
  2. Миддл: я знаю все
  3. Сеньор: я ничего не знаю

Чем больше мы знаем, тем отчетливее осознаем, что гораздо больше областей нам неподвластны. К чему это я? Да к тому, что сколько бы хорошим и уверенным в себе ни был программист, он по-прежнему может совершить ошибку. Даже лучшие от этого не застрахованы. И это нормально.

На самом деле, всем плевать, насколько вы опытны и уверены в себе. Важно только то, что результат вашей работы можно эффективно использовать, а о применимости результата вам расскажут ваши тесты.

На мой взгляд, самое главное правило хорошего программиста звучит следующим образом:

Закрывая тикет и отправляя задачу в тестирование, вы гарантируете, что:

  1. Тестирование не выявит багов в вашем коде.
  2. Тестирование не выявит проблемы регрессии, вызванные вашими изменениями.
  3. Закрытый вами тикет никогда не окажется в статусе Reopened.

Прогоняя юнит- и интеграционные тесты, вы становитесь в состоянии дать эту гарантию.

Тесты отнимают время

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

Требуют ли тесты поддержки?

Довольно много раз я слышал мнение, что юнит- и интеграционные тесты требуют поддержки. Зачастую именно по этой причине их не любят.

Лично я ничего более глупого не слышал. Тесты не требуют абсолютно никакой поддержки – вы их написали и радуетесь. Если во время рефакторинга или добавления новой функциональности вы случайно что-то сломаете, тесты вам об этом расскажут (при условии, что вы нашли тот самый баланс). Как правило, бизнес-требования по уже написанной функциональности не меняются каждую неделю, поэтому и тесты нет нужды обновлять.

Да, если вам все-таки приходится обновить логику приложения в каком-то месте, то придется обновить и соответствующие тесты. Но это один случай на сто, в остальных 99 случаях тесты просто делают свою работу и не требуют внимания.

Код и тесты – единое целое

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

Вспомнить правило

Закрывая тикет и отправляя задачу в тестирование, вы гарантируете, что:

  1. Тестирование не выявит багов в вашем коде.
  2. Тестирование не выявит проблемы регрессии, вызванные вашими изменениями.
  3. Закрытый вами тикет никогда не окажется в статусе Reopened.

Исходя из этого правила можно сделать вывод, что код и тесты на этот код – единое целое. Не стоит рассматривать тесты как какую-то необязательную надстройку над единственно важной вещью – кодом.

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

Конечно, есть и QA-отдел. Но следует помнить, что только хреновый программист отдает в QA сырой код, который он даже не удосужился проверить и запустить. Цель хорошего программиста – отдать в QA только тот код, в котором он уверен.

Отдавая сырой код в QA и приступая к следующей задаче, вы вовсе не экономите время и не становитесь более эффективным. Наоборот, вы рискуете потратить гораздо больше времени, чем это необходимо, если тестировщики вернут вам задачу на доработку. Тогда вам придется снова погружаться в контекст, искать причину ошибки и отправлять задачу на вторую итерацию. А это, как я уже говорил выше, недопустимо для хорошего разработчика.

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

Время – цена тестирования. Но помните, что время может быть как потраченным, так и сэкономленным.

Тесты на библиотеки

Стоит ли тестировать тривиальные функции и методы библиотек, которые вы используете?

Допустим, есть такая функция:

public MyObject getInstance()
    return new MyObject();
}

Лично я не стал бы покрывать ее тестами. Тут нет бизнес-логики, и сама функция слишком тривиальна. Суть тестирования не в том, чтобы покрыть 100% строк и веток, а в нахождении нужного баланса – что стоит тестировать, а что нет.

Библиотеки, которые вы используете в своих проектах, уже оттестированы их разработчиком и, на первый взгляд, не требуют дополнительного тестирования с вашей стороны. Но всегда может настать момент, когда вы обновляете версию библиотеки, а там старый метод начинает вести себя по-другому. С этой позиции неплохо бы иметь чисто поверхностные тесты на те функции библиотеки, которые вы используете.

Либо же, как рекомендовал Роберт Мартин, вы можете писать адаптеры к этим библиотекам, и покрывать тестами эти самые адаптеры. Это поможет вам как отловить какие-либо изменения в поведении библиотек, так и заменять либо удалять их в принципе.

Happy Path тестирование

Happy Path, когда вы тестируете только положительные сценарии, может подойти только к поверхностному тестированию библиотек. Когда вы тестируете свой код, вы должны хотеть его сломать.

Представьте, как собирается тестовый автомобиль на конвейере. При happy path после сборки тестировщик просто покрутил бы руль, поддал бы немножко газку и удостоверился бы, что машина тормозит. Но достаточно ли этого? А как же жесткое тестирование подвески, резкий старт и разгон, устойчивость в поворотах и, конечно же, крэш-тест?

В тестах вы должны ломать свой код. Не переживайте, если код написан хорошо, то он не повредится.

Проблема вовсе не в тестах

Есть еще одно расхожее мнение – проблема вовсе не в тестах, а в человеке, который сидит по ту сторону монитора. Иначе говоря, если руки растут из нужного места, а не как обычно, то вероятность ошибок автоматически исключается, и в тестах нет нужды.

Проблема в человеке по ту сторону монитора, безусловно, есть. Люди – вообще источники всех проблем. И, повышая качество исполнителя, можно достичь очень даже впечатляющих результатов.

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

Простейший пример

Буквально вчера я написал юнит-тест на кусок своего кода в полной уверенности, что увижу зеленую галочку. Однако, тест упал. И не просто упал, а свалился с позорным NullPointerException. Для более-менее опытного программиста словить NPE – это вообще стыд и срам. Если вы получаете NPE, вам следует встать коленками на горох и самолично выпороть себя на глазах у всех коллег.

А вот тот самый кусок кода (упрощенный), на котором упал тест:

public void doSomething(Boolean condition) {
    if (condition) {
        // handle condition
    }
    // handle
}

Можете сразу заметить проблемное место?

Суть в том, что в метод приходит булев флаг, в зависимости от которого нужно выполнить какое-то дополнительное действие. Флаг этот не является обязательным, то есть его может не быть вовсе, и в таком случае мы должны предполагать, что он эквивалентен значению false. И тест как раз проверял вариант, когда этот флаг не передается:

doSomething(null);

Разумеется, NPE упал на этой строке:

if (condition)

Когда я поправил код, все заработало:

if (Boolean.TRUE.equals(condition))

Глаза замыливаются, и вы можете начать работать с big-B Boolean как с обычным boolean. Но у тестов есть еще одно крутое преимущество – у них ничего не замыливается, и они не ошибаются.

Вывод: тесты – ваши друзья.

Главное правило хорошего программиста

Давайте еще раз вспомним главное правило хорошего программиста.

Закрывая тикет и отправляя задачу в тестирование, вы гарантируете, что:

  1. Тестирование не выявит багов в вашем коде.
  2. Тестирование не выявит проблемы регрессии, вызванные вашими изменениями.
  3. Закрытый вами тикет никогда не окажется в статусе Reopened.

А соблюдать это правило вам поможет одна простая аксиома:

Тесты в разумном количестве с высокой вероятности дадут гарантию того, что задача не будет возвращена на доработку.

Заключение

Тесты – не панацея. Сами по себе они не несут никакого смысла. И только грамотное сочетания программного кода и разумного количества тестов поможет достичь нужного результата. В каждой ситуации вам стоит решать, где и как вы будете (или не будете) писать тесты на свой код, чтобы достичь цели.

В общем и целом, правило простое – чем больше тестов, тем надежнее. Однако, крайности – это всегда плохо, так что вам всего лишь стоит найти баланс 🙂

Понравилось? Подписывайтесь на меня в соцсетях!

 
Facebook
Twitter
VK

guest
2 Комментариев
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Алексей
Алексей
3 месяцев назад

Как-то начав работать на большом продукте пришел к такому же выводу, QA покрывают мало случаев и при хорошем покрытии автотестами можно обойтись небольшой командой проверяющей сложные случаи. Разработчику нужно уметь и писать код и автотесты — без них редко принимают таску, делать все быстро и качественно. Но перейдя в другую область программирования увидел очень забавную вещь. Можно быть не сеньор/лид, а простым мануал QA и получать те же деньги в Самаре — до 600-1000$ (как 2-3 летний разработчик особо не смотрящий по сторонам). Особенно если сидеть долго на одном месте и можно не париться над изучением языков программирования, автоматизации тестов и прочего. Команды по 30-40 человек на одном проекте. Пришел, внимательно прошел 3-5 E2E и пошел домой. Даже английский Elementary пойдет. В таких проектах мануал QA еще долго будут счастливо жить, сидеть на митингах и лутать зп.

Social media & sharing icons powered by UltimatelySocial
2
0
Нравится? Оставьте комментарий!x
()
x