И здесь, и в других местах в Сети есть масса статей, пропагандирующих автоматическое тестирование вообще и unit-тесты в частности. В статьях расписываются преимущества тестирования, использование его для устранения хрупкого кода, увеличения качества, миграции со старых систем на новые, рефакторинга. И, одновременно, нигде почти не упоминается об их недостатках, а ведь в инженерии нет "серебряных пуль"! На самом деле "серебряные пули" есть, но их изобрели ещё первые инженеры, и они воспринимаются нами как скучные банальности: "мойте руки перед едой", "вытирайте ноги", "структурируйте код", "не пишите без отступов", "локализируйте состояние" и т.д. Тем не менее, тесты — это не "серебряная пуля", а один из эффективных и широко используемых инструментов, а значит, у него есть недостатки. В этой заметке я попытаюсь структурировать и выписать именно недостатки тестов, в основном юнит-тестов. О достоинствах я постараюсь не писать, ведь об этом уже и так много материалов, только руку протяни. Разумеется, где-то я неизбежно что-то важное забуду, а где-то буду чересчур сгущать краски. Поэтому просьба рассматривать эту статью скорее как приглашение к беседе, чем что-то законченное. С моей точки зрения тема вполне назрела, и поэтому очень хотелось бы её обсудить в деталях. Почему функциональное программирование? Так тестируем же мы почти исключительно функции. Это, в общем, не секрет, что даже 100% покрытие тестами не гарантирует правильного поведения программы. Для примера глянем на код: Мы прошли по всем веткам, всё замечательно, но вот доказали ли мы, что функция 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. У программистов же далеко не всегда есть возможность и умение вникнуть во все эти дисперсии, распределения... Кроме того, связь генераторов и устройства тестируемого кода — это ведь хрупкость тестов: вы слегка соптимизируете алгоритм, распределения поплывут, и генераторы нужно будет опять править. К счастью, это похоже случается редко. И, наконец, контрольный вопрос: что покажет тестирование свойства ниже? Так что запишем для себя, что добиться хорошего покрытия не так-то просто - требуется глубокое понимание того, что же делает тестируемый код, какое пространство входных параметров, как оно отражается в потрохах тестируемого кода. Эту нехитрую, но глубокую мысль я увидел здесь, на Хабре, в одном из комментариев ув. Jef239. Заметьте, что если юнит-тест мы используем для проверки нового кода, то желательно, чтобы код и проверяющий его тест писали разные люди. Ведь если программист по какой-то причине решит, что месяц Январь следует за Февралём, то он так и напишет в двух местах, причём ещё и методом copy-paste: То есть, говоря языком статистики, происходит "систематическая" ошибка. Суть проблемы заключается в том, что программист неправильно понимает контекст, в рамках которого работает его программа: либо предметную область, либо постановку задачи. Разумеется, системы типов или даже верификация тут тоже не помогут - спасение именно во "взгляде со стороны". Другой человек, как правило, имеет слегка отличающуюся точку зрения, поэтому он не сделает именно эту ошибку. А значит будет выявлено несовпадение, и тексты исправят. Представьте, к примеру, что тест написан другим программистом, у которого год начинается с марта. Они поспорят, может быть даже подерутся, но в конце-концов непременно отыщут истину! К сожалению, индустрия по разным причинам массово игнорирует этот отличный способ извлечь пользу из тестов — как правило, код и тесты к нему пишет один и тот же человек. Собственно, заголовка может быть и достаточно — большая часть программистов может выписать все минусы кода сразу, не приходя в сознание после сна. То, что тесты — это код, текст на каком-то языке программирования, означает: Кто-то его должен написать. Как правило, более-менее исчерпывающий набор тестов для функции, написанной на "интерпретируемом" языке, в разы больше по объёму, чем сама эта функция. Кто-то его должен отладить. Как это ни смешно, в тестах тоже бывают ошибки. Их, конечно, отладить проще, тем не менее, это таки надо сделать. Кто-то должен отрецензировать эти тесты, потратить уйму времени, ведь тесты длинные (см. пункт 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 и проверок компилятора, а оканчивающихся полномасштабным клиентским тестированием. В этой системе, каждый шаг водопада ли, спирали - это очередной фильтр, убирающий часть ошибок. Разумеется, чем проще, быстрее и, одновременно, грубее фильтр, тем раньше он должен применяться. И вот в этой системе, как мне кажется, юнит-тесты должны занимать место после компиляции, но до полноценного тестирования программы разработчиком. То есть, они должны находиться в конвейере после системы типов, а не вместо неё. Как, впрочем, всё и устроено в типичном функциональном программировании. Так что у нас всё хорошо!