Найти тему
Сделай игру

Тестирование: бессмысленное и беспощадное

Оглавление

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

Давайте же разберёмся, что нам вообще даёт тестирование.

Тестирование по-взрослому
Тестирование по-взрослому

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

Скажем, у нас есть функция, которая умножает любое число на 2; для неё создаётся тест, который проверяет работу данного функционала. Начинающий разработчик проверит, что f(2) == 4, f(10) == 20 и на этом успокоится. Более опытный добавит тест на переполнение (это когда результат вычисления превышает предельно возможное значение), заставив разработчика (скорее всего себя же) доработать функцию так, чтобы её вызов не приводил к критическому завершению приложения.

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

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

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

Считается, что есть некоторый фиксированный набор последовательности адреса, но он может немного меняться в зависимости от провайдера: где-то могут быть запрещены цифры в имени, где-то может быть много уровней поддоменов и всё в таком духе.

Разработчик, может быть, встречал, что адреса электронной почты имеют, максимум, 2 уровня доменов (name@subdomain.domain.ext), а домен верхнего уровня содержит 2 или 3 буквы. И тут появляется пользователь, который имеет цепочку в 14 поддоменов с доменом верхнего уровня torrent. У него пройти проверку с таким адресом нет ни шанса.

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

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

Первые трудности

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

Вернёмся к разработке кода. Как мы уже обсудили, тестирование нужно прежде всего, чтобы удостовериться, что код работает верно. Существует понятие идемпотентности, предполагающая, что многократно выполненная операция всё время будет возвращать один и тот же результат (скажем, запрос сайта всегда возвращает его заглавную страницу). Однако есть функциональности (методы), которые данным свойством не обладают, более того, могут со временем накапливать погрешность, что гарантировано приводит в какой-то момент к отличному от ожидаемого результата.

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

На моей практике был забавный случай: довольно веб тяжёлое приложение загружалось по сети, причём загружалось и выполнялось частями. Если скорость оказывалась недостаточно высокой, то приложение просто не запускалось, т.к. к моменту выполнения кода, часть функционала, на которую тот код ссылался, ещё не успевала загрузиться.

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

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

Стало быть, вариант просто написать много тестов для всяких случаев - лишён всякого смысла.

Плановый подход

В большинстве команд, с которыми мне довелось поработать, существовал плановый подход: покрыть тестами какой-то процент кодовой базы (20, 40 60 итп). И данный подход был полностью провальным: тестами покрывалась так функциональность, которую легко было покрыть; сложную оставляли на потом и, нередко, не реализовывали.

Часто использовался такой подход: вот у нас есть функции, которые что-то выполняют и используются внутри других; давайте мы их покроем тестами, а, значит, всё, что на них опирается, будет более стабильным. Подход неплохой, но его работоспособность сомнительна.

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

Короче говоря, требование к процентному покрытию тестами решали только одну задачу - отчётность перед начальством; во всём остальном результат не достигался.

Взвешенный подход

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

У тестирования есть две цели:

  1. Правильность работы кода
  2. Правильность работы приложения

При этом следует понимать, что существует три типа ошибок, которые могут возникнуть: критическая (серьёзный сбой приложения), существенная (потеря части данных при сохранении работы приложения) и незначительная. И ошибки эти могут возникнуть как на стороне реализации логики программы, так и при нарушении пользователем предполагаемых правил работы с приложением.

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

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

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

Заключение

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

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