[Из песочницы] Случайность в автотестах
Введение Когда несколько лет назад я написал свой первый автотест, он выглядел следующим образом. В цикле 100 раз доставал из базы случайного пользователя, проводил над ним требуемую операцию и проверял, что результат меня устраивает. Это казалось достаточно логичным: не могу же я проводить тест на одном пользователе, этого недостаточно, это ничего не докажет.С тех пор прошло значительное время, я успел поработать над несколькими разными проектами на разных языках и даже сменить команду. Сегодня я могу с уверенностью сказать: вы не должны использовать случайность в своих автотестах, кроме случаев, которые будут оговорены отдельно. И я расскажу почему.
Пример Для примеров буду использовать простейшую функцию, которая возводит число в квадрат, но сохраняет знак. На Ruby, например, это выглядело бы так: def smart_sqr (x) x > 0? x*x: -x*x; end Легко представить, как будет выглядеть тест для такой функции. Я просто возьму некоторые контрольные примеры и сравню значение smart_sqr () на этих примерах с контрольными: assert_equal (smart_sqr (4), 16); Вопрос — по какому принципу мне выбирать значения.«Преимущества» случайных значений Почему я стал выбирать случайные значения в тот раз, когда писал свой первый автотест? Почему программисты продолжают использовать случайные значения в своих тестах? Их (и мою) логику легко понять: один эксперимент ничего не доказывает, подход — сугубо вероятностный: чем больше различных вариантов протестировано, тем лучше.Все не совсем так. Как правило, в современных системах теоретическое доказательство верности программ (а) практически невозможно и (б) не требуется. Вся программа базируется на гипотезе самого программиста о том, что она делает то, что должна. Доказать эту гипотезу — невозможно, однако с помощью тестов я могу свести свою программу к набору гипотез попроще, неформальное понимание которых было бы доступней.
Что я имею в виду? Для функции, написанной выше, мне, в некотором смысле, очевидно, что она ведет себя одинаково на всех положительных числах. Под словом «очевидно» я имею в виду ту самую гипотезу, на которой строится моя вера в то, что моя программа вообще работает как надо (это общая проблема всех инженерных дисциплин, что некоторые вещи приходится делать на глаз). В отсутствии каких-либо гипотез любое тестирование было бы бесполезно; мне помогло бы только формальное доказательство (которое, повторюсь, на грани невозможного).
В присутствии же гипотезы достаточно проверить работоспособность функции лишь на одном положительном числе, чтобы удостовериться, что она верно функционирует на всех.
Итак, мне не нужны случайные значения, чтобы проверить работоспособность моей функции. Я просто использую все граничные значения (верней те, которые мне таковыми кажутся) и по одному значения для каждого класса значений, которые, по моему мнению, ведут себя одинаково. В реальности для нашей функции я бы использовал значения –7, 0 и 13. Ваше мнение о граничных условиях может отличаться от моего, и это нормально. Например, единица ведет себя несколько отличным образом: ее квадрат равен исходному значению.
Также многим программистам может казаться, что бессмысленно прогонять тест на все тех же значениях вновь и вновь, ведь их результат не может измениться. Это действительно так, но задача автотестов не искать ошибки в уже запущенных программах, их задача — реагировать на изменение кода. Если вы не меняете код, то тесты повторно вообще можно не запускать.
Недостатки случайных значений Если вы используете случайные значения в тестах, вы можете столкнуться с рядом проблем.Во-первых, тест может вести себя непостоянно. Это теоретически неприемлемо, а также может вызывать массу проблем на практике (например, ваша система, прогоняющая тесты, может решить, что вы все сломали и исправили просто от того, что тест моргнул красным, и выполнить какие-то нежелательные действия). Тест должен реагировать на изменение кода и только на него. Падение тестов из-за нарушения среды и так является проблемой, незачем усугублять ее, увеличивая влияние среды путем добавления тестов, зависящих от состояния генератора случайных чисел.
Во-вторых, отладка таких тестов может быть серьезной проблемой. Если значения, на которых упал тест, не сохранились, то такой результат вообще может оказаться бесполезным.
В-третьих, код теста может лишиться своей ясности при добавлении в него случайных чисел. Чему должен быть равен квадрат случайного числа в нашем примере? Квадрату этого случайного числа? При таком подходе код теста в точности повторит код функции (о, кстати, отличная идея, воспользуемся ей же для проверки!).
Но в моем случае… Да, в некоторых случаях использование случайных значений может оказаться полезным. Но вы должны относиться к этому крайне настороженно. В некоторых языках возможен вызов приватных методов класса. И это тоже иногда может оказаться полезным. Но это не повод не подумать семь раз, а потом еще два, перед тем как использовать эту возможность.Я приведу пару случаев, в которых, на мой взгляд, можно закрыть глаза на использование случайных значений. Это не полный перечень. Если здравый смысл подсказывает вам, что вы можете или даже должны нарушить правило, которое я озвучил выше, нарушайте его.
Вы ищете ошибку. Вы знаете, что в вашем коде есть ошибка, она иногда проявляет себя в продакшене. Обнаружить ее, следуя логике, не удается. Вы можете попробовать найти ее перебором. Для этого вы можете воспользоваться вашей системой автоматического тестирования, которая при каждом запуске будет проверять какое-то число случайных значений. В этом решении все хорошо, кроме того, что это — не совсем автотесты. Это просто скрипт для поиска ошибок, который вы интегрировали в вашу систему автоматического тестирования для удобства. Подумайте дважды: возможно, эта интеграция вам и вовсе не нужна.
Исходные данные слишком велики. Может случиться так, что вам сравнительно безразличны исходные данные, но их объем таков, что хранить их — затруднительно. В этом случае вы можете создать их налету, хотя хранение предгенеренных данных все равно предпочтительней. Также есть вариант с автоматической неслучайной генерацией.
Если вы все же решили использовать случайные значения в ваших тестах, вам необходимо сохранять значение, которым был инициализирован генератор случайных чисел. Если вы используете случайность в сторонних модулях или системах (например, внутри вашей базы данных), это может вызывать серьезные затруднения технического характера.
В заключение Хотелось бы добавить, что автоматическое тестирование, на мой взгляд, — одна из самых плохо исследованных и формализованных областей в программировании. На любой вопрос можно получить диаметрально противоположные ответы, а по любому поводу услышать взаимоисключающие мнения. Даже точки зрения уважаемых и признанных специалистов могут существенно разниться. Если вы прямо сейчас попробуете поиском найти ответ на вопрос, обсуждаемый в моей статье, вы услышите тысячи точек зрения, начиная от «случайность необходима» и заканчивая «случайность недопустима». Я попытался как можно более понятно разъяснить свои идеи, т. к. простое формулирование принципов в области автотестирования давно уже не работает.