[Функциональное программирование] Другая сторона медали или про недостатки юнит-тестирования
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
ВведениеИ здесь, и в других местах в Сети есть масса статей, пропагандирующих автоматическое тестирование вообще и unit-тесты в частности. В статьях расписываются преимущества тестирования, использование его для устранения хрупкого кода, увеличения качества, миграции со старых систем на новые, рефакторинга. И, одновременно, нигде почти не упоминается об их недостатках, а ведь в инженерии нет "серебряных пуль"!На самом деле "серебряные пули" есть, но их изобрели ещё первые инженеры, и они воспринимаются нами как скучные банальности: "мойте руки перед едой", "вытирайте ноги", "структурируйте код", "не пишите без отступов", "локализируйте состояние" и т.д. Тем не менее, тесты — это не "серебряная пуля", а один из эффективных и широко используемых инструментов, а значит, у него есть недостатки.В этой заметке я попытаюсь структурировать и выписать именно недостатки тестов, в основном юнит-тестов. О достоинствах я постараюсь не писать, ведь об этом уже и так много материалов, только руку протяни. Разумеется, где-то я неизбежно что-то важное забуду, а где-то буду чересчур сгущать краски. Поэтому просьба рассматривать эту статью скорее как приглашение к беседе, чем что-то законченное. С моей точки зрения тема вполне назрела, и поэтому очень хотелось бы её обсудить в деталях.Почему функциональное программирование? Так тестируем же мы почти исключительно функции.Прощание с иллюзиями или 33 банальностиЭто, в общем, не секрет, что даже 100% покрытие тестами не гарантирует правильного поведения программы. Для примера глянем на код:
def f( a, b):
x = 0
if a:
x += 2
else:
x += 0
if b:
x += 2
else:
x += 0
return x
# и полное покрытие
assert f(True, False) == 2
assert f(False, True) == 2
Мы прошли по всем веткам, всё замечательно, но вот доказали ли мы, что функция f всегда возвращает двойку?Да, можно сказать, что это не 100% покрытие всех путей выполнения, но, боюсь, это автоматически определить, действительно ли мы прошли по всем возможным путям, невозможно. Обычные юнит-тесты неизбежно проверяют лишь небольшое кол-во точек в пространстве параметров тестируемой функции. Их можно использовать для проверки каких-то очень важных особых случаев, но считать это покрытие "полным" несколько наивно.Поэтому перейдём к property-base testing: это знаменитый QuickCheck из Haskell, GAST из Clean, Kotlintest, QCheck из Ocaml, Hypothesis для Python'а и другие. Они покрывают сразу большое и случайное количество параметров тестируемой функции. Увы и ах, но это тоже не серебряная пуля: у них есть свои особенности, свои проблемы и области применимости.На языке физики, в первом приближении эти библиотеки гоняют Монте-Карло, перебирая разные пути выполнения или разные варианты редукции графа, как траектории пролёта элементарной частицы. Прямо как в Geant4 мы задаём "источники" (генераторы), "рисуем геометрию" (записываем свойства) и запускаем расчёт (когда на 5 секунд, а когда и на сутки).И ровно также, как в физике высоких энергий, из-за относительно малого количества запусков мы можем пропустить что-то очень интересное. Мне доводилось видеть, как даже 20 000 прогонов не хватало, чтобы найти ошибку в функции, перемножающей полиномы в символьном виде — требовалось 50 000 запусков. И даже миллион траекторий не может гарантировать того, что не вылезет ошибка на втором миллионе.В общем, property-based testing — это эксперимент, который надо уметь ставить, и его результаты тоже надо уметь обрабатывать. Об этом прекрасно рассказал автор QuickCheck в видео John Hughes — Building on developers' intuitions (...) | Lambda Days 19. У программистов же далеко не всегда есть возможность и умение вникнуть во все эти дисперсии, распределения...Кроме того, связь генераторов и устройства тестируемого кода — это ведь хрупкость тестов: вы слегка соптимизируете алгоритм, распределения поплывут, и генераторы нужно будет опять править. К счастью, это похоже случается редко.И, наконец, контрольный вопрос: что покажет тестирование свойства ниже?
propertyDoubleEq :: Double -> bool
propertyDoubleEq x = (x == x)
Так что запишем для себя, что добиться хорошего покрытия не так-то просто - требуется глубокое понимание того, что же делает тестируемый код, какое пространство входных параметров, как оно отражается в потрохах тестируемого кода.2 + 2 = 5? А если протестирую?Эту нехитрую, но глубокую мысль я увидел здесь, на Хабре, в одном из комментариев ув. Jef239.Заметьте, что если юнит-тест мы используем для проверки нового кода, то желательно, чтобы код и проверяющий его тест писали разные люди. Ведь если программист по какой-то причине решит, что месяц Январь следует за Февралём, то он так и напишет в двух местах, причём ещё и методом copy-paste:
string monthName(unsigned int n) {
static vector<string> months = {"Февраль", "Январь", ... };
return months[n % months.size()];
}
void testMonthNames() {
assert( monthName(0) == "Февраль");
...
}
То есть, говоря языком статистики, происходит "систематическая" ошибка. Суть проблемы заключается в том, что программист неправильно понимает контекст, в рамках которого работает его программа: либо предметную область, либо постановку задачи.Разумеется, системы типов или даже верификация тут тоже не помогут - спасение именно во "взгляде со стороны". Другой человек, как правило, имеет слегка отличающуюся точку зрения, поэтому он не сделает именно эту ошибку. А значит будет выявлено несовпадение, и тексты исправят.Представьте, к примеру, что тест написан другим программистом, у которого год начинается с марта. Они поспорят, может быть даже подерутся, но в конце-концов непременно отыщут истину!К сожалению, индустрия по разным причинам массово игнорирует этот отличный способ извлечь пользу из тестов — как правило, код и тесты к нему пишет один и тот же человек.Юнит-тесты — это кодСобственно, заголовка может быть и достаточно — большая часть программистов может выписать все минусы кода сразу, не приходя в сознание после сна. То, что тесты — это код, текст на каком-то языке программирования, означает:
- Кто-то его должен написать. Как правило, более-менее исчерпывающий набор тестов для функции, написанной на "интерпретируемом" языке, в разы больше по объёму, чем сама эта функция.
- Кто-то его должен отладить. Как это ни смешно, в тестах тоже бывают ошибки. Их, конечно, отладить проще, тем не менее, это таки надо сделать.
- Кто-то должен отрецензировать эти тесты, потратить уйму времени, ведь тесты длинные (см. пункт 1). Ужасный расход средств, дорогого времени старших программистов, тимлидов. Так и в трубу вылететь недолго, если это стартап!
Ладно, шучу, никто, конечно же, не читает и не проверяет код тестов. Разве что автоматически. То есть, качество кода тестов, как правило, ниже качества тестируемого кода. В любом случае, оно если и выше, то совершенно случайно.
- Кто-то должен поддерживать тесты. Если мы говорим не об одной из Стандартных Библиотек, тестируемый код и требования к нему будут меняться. Непонятно столько раз за время жизни кода и программиста, но обязательно. Значит придётся подправлять и юнит-тесты, разбираться в них, а ведь кода там много, значит опять потери.
Все пункты выше — это расходы времени программистов разного уровня, то есть деньги.А ещё, в пессимистичном случае, юнит-тесты — это огромная библиотека относительно плохого кода, в котором мало кто разбирается, но который нужно поддерживать.Юнит-тесты — это запускающийся кодОпять, пессимисту достаточно заголовка, а дальше он сам придумает. Но, всё же, раскроем тему.Мы пишем юнит-тесты не просто так, и даже не только для того, чтобы их листинги показывать в отчётах и на совещаниях, прогонять через компиляторы и линтеры. И для этого, конечно, тоже, но если задвинуть всевозможную теорию игр в корпоративных джунглях, юнит-тесты должны выполняться на компьютерах и печатать заветные зелёные буковки. Но вот тут появляются несколько неприятностей:
- Прогон тестов занимает физическое время. То есть, программист вынужден оторваться от задачи в лучшем случае на несколько секунд, а в худшем — пойти пить чай, т.к. до вечера тесты всё равно не пройдут. А ведь время итерации write-check-correct loop — это важнейшая характеристика, напрямую влияющая на производительность программиста.
Кстати, полный набор тестов компилятора Ocaml прогоняется примерно за час — не то, чтобы критично, но при работе с какой-то частью компилятора приходится делать "стенд" — выделять отдельный тест(ы) и прогонять его за секунды. Хотя все тесты совершенно по-делу, отмечу для протокола, что весь компилятор с нуля собирается минуты за две.
- Прогон тестов занимает машинное время с соответствующим расходом машин и электроэнергии. Где-то это не критично, а где-то таки да.
- Интеграция медленных тестов и CI замедляет процесс рецензирования - если рецензент в какой-то момент проверит код при неоконченном CI процессе, а какой-то тест из набора через пол часа провалится, то рецензенту придётся повторять проверку. И наоборот, если рецензент нашёл опечатку в комментарии, то после исправления тесты придётся прогонять снова, и всё это время автор изменения не сможет выкинуть свой PR из головы.
Резюмируя, процесс разработки замедляется из-за необходимости прогона юнит-тестов. Разработка замедляется как для программиста индивидуально, так и для всей группы.Юнит-тесты — это работающий код без пользователейОчень важный момент заключается в том, что по-сути, у юнит-тестов нет пользователя, кроме их автора. То есть, нет человека, который в них заинтересован, готов их подправлять, поддерживать. У обычной программы, как правило, есть пользователи, которые либо сами правят, либо находят ошибки и пишут автору. За счёт этого даже какие-то древние вещи вроде WindowMaker, Quake I, Heroes 2 до сих пор живут и здравствуют, портируются на другие системы, развиваются. Даже если программа заморожена, как TeX, с пользователями она живёт, а без пользователей умирает.Это же относится и к таким программам, как юнит-тесты. Это ведь программы, а не просто письмена на жёстком диске. А значит, они тоже могут деградировать и портиться — в компьютерном мире как нигде "всё течёт, всё меняется".Как сказано выше, неизбежно качество и документация юнит-тестов не может, да и не должна быть выше, чем качество основного кода. Значит, их труднее поддерживать, а какого-то положительного эффекта для работника от их обновления почти нет. This does not add business value, right?Смотрите, если вы — разработчик, и у вас какой-то тест протух по каким-то внешним причинам, и не пускает ваши изменения в общий репозиторий, то у вас есть два выхода: исправить тест или обойти его. Причём соблазн обойти иногда крайне велик — при переходе, скажем, с Python 2 на Python 3 или серьёзном обновлении библиотек Boost, портирование даже короткой программы может занять дни. И всё это время вам будут капать на мозги, что же это ваша работа не сделана!В результате мы видим печальную картину: когда автор тестов увольняется из конторы, через пару лет сотрудникам зачастую проще удалить файл с тестами, чем исправлять в них какую-то ошибку. И ведь удаляют.То есть, тесты гарантированно "защищают" основной код программы лишь ограниченное время — год, два, три.ЗаключениеВроде бы я перечислил всё, что накопилось на душе. Наверняка это не всё, и наверняка часть из претензий надумана. Например, я совершенно не рассмотрел интеграционные тесты, не затронул mutation testing, без сомнения упустил ещё что-то важное.Если сжать вышенаписанное до одного абзаца, то можно сказать, что у юнит-тестов есть много отрицательных черт, в основном связанных с тем, что это — программы, которые необходимо поддерживать, развивать, запускать. Как всякая программа, они не могут быть написаны совсем без ошибок, их время выполнение подчас сильно бьёт по скорости рабочих циклов программистов и команд. Тесты, как правило, имеют достаточно ограниченное время жизни, поэтому они защищают код лишь до какого-то момента.Разумеется, юнит-тесты — это мощнейший инструмент, но как у любого инструмента, их применение имеет свою цену. Лишь "идеальная система – это система, которой нет, а её функции выполняются".Есть взгляд на разработку ПО как на "систему фильтров ошибок", начинающуюся от критики в голове автора, скептического рассматривания карандашных набросков, продолжающихся в виде подсказок IDE и проверок компилятора, а оканчивающихся полномасштабным клиентским тестированием. В этой системе, каждый шаг водопада ли, спирали - это очередной фильтр, убирающий часть ошибок. Разумеется, чем проще, быстрее и, одновременно, грубее фильтр, тем раньше он должен применяться. И вот в этой системе, как мне кажется, юнит-тесты должны занимать место после компиляции, но до полноценного тестирования программы разработчиком.То есть, они должны находиться в конвейере после системы типов, а не вместо неё. Как, впрочем, всё и устроено в типичном функциональном программировании. Так что у нас всё хорошо!
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Программирование, Haskell, Функциональное программирование] Почему я считаю Haskell хорошим выбором с точки зрения безопасности ПО? (перевод)
- [JavaScript, Программирование, Алгоритмы, Функциональное программирование] Решаем вопрос сортировки в JavaScript раз и навсегда
- [Программирование, Компиляторы, Функциональное программирование, Искусственный интеллект] Тестирование синтаксиса языка программирования с необычной концепцией
- [Разработка веб-сайтов, Программирование, Haskell, Функциональное программирование] Создаем веб-приложение на Haskell с использованием Reflex. Часть 3
- [Тестирование IT-систем, Анализ и проектирование систем, Проектирование и рефакторинг, TDD, Отладка] Почему большинство юнит тестов — пустая трата времени? (перевод статьи) (перевод)
- [Python, Функциональное программирование] Основы функционального программирования на Python
- [Python, Функциональное программирование] Функциональное ядро на Python
- [Программирование, Haskell, Функциональное программирование, Rust] Как мы выбираем языки программирования в Typeable
- [PHP] Не мокайте то, чем вы не владеете (перевод)
- [Тестирование IT-систем, Go, Тестирование веб-сервисов] Подсказки по написанию тестов в приложениях на Go
Теги для поиска: #_funktsionalnoe_programmirovanie (Функциональное программирование), #_unittesting, #_funktsionalnoe_programmirovanie (
Функциональное программирование
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:37
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
ВведениеИ здесь, и в других местах в Сети есть масса статей, пропагандирующих автоматическое тестирование вообще и unit-тесты в частности. В статьях расписываются преимущества тестирования, использование его для устранения хрупкого кода, увеличения качества, миграции со старых систем на новые, рефакторинга. И, одновременно, нигде почти не упоминается об их недостатках, а ведь в инженерии нет "серебряных пуль"!На самом деле "серебряные пули" есть, но их изобрели ещё первые инженеры, и они воспринимаются нами как скучные банальности: "мойте руки перед едой", "вытирайте ноги", "структурируйте код", "не пишите без отступов", "локализируйте состояние" и т.д. Тем не менее, тесты — это не "серебряная пуля", а один из эффективных и широко используемых инструментов, а значит, у него есть недостатки.В этой заметке я попытаюсь структурировать и выписать именно недостатки тестов, в основном юнит-тестов. О достоинствах я постараюсь не писать, ведь об этом уже и так много материалов, только руку протяни. Разумеется, где-то я неизбежно что-то важное забуду, а где-то буду чересчур сгущать краски. Поэтому просьба рассматривать эту статью скорее как приглашение к беседе, чем что-то законченное. С моей точки зрения тема вполне назрела, и поэтому очень хотелось бы её обсудить в деталях.Почему функциональное программирование? Так тестируем же мы почти исключительно функции.Прощание с иллюзиями или 33 банальностиЭто, в общем, не секрет, что даже 100% покрытие тестами не гарантирует правильного поведения программы. Для примера глянем на код: def f( a, b):
x = 0 if a: x += 2 else: x += 0 if b: x += 2 else: x += 0 return x # и полное покрытие assert f(True, False) == 2 assert f(False, True) == 2 propertyDoubleEq :: Double -> bool
propertyDoubleEq x = (x == x) string monthName(unsigned int n) {
static vector<string> months = {"Февраль", "Январь", ... }; return months[n % months.size()]; } void testMonthNames() { assert( monthName(0) == "Февраль"); ... }
=========== Источник: habr.com =========== Похожие новости:
Функциональное программирование ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:37
Часовой пояс: UTC + 5