Хороший тест

Можно ли определить объективные критерии для признания тестирующего кода хорошим или плохим? Глядя на тесты, которые я писал несколько лет назад могу с уверенностью сказать, что, как минимум, плохие тесты вычислить легко) В этой заметке рассмотрим сперва основу основ - принципы F.I.R.S.T, а продолжим моими личными размышлениями на тему...

Скорость

Хороший тест просто обязан быть быстрым. Никому не нужна сотня тестов, прогон которых занимает половину дня. Смысл TDD заключен в идее постоянного запуска тестов в процессе разработки. Прогоны тестов происходят буквально каждые пару минут. И если тесты будут выполняться пару минут... ну вы поняли) Никаких точных цифр нет, никто, насколько мне известно, не проводил масштабных исследований на эту тему, потому в качестве примера сошлюсь на собственный опыт: ~1000 тестов занимает 15-17 секунд (правда при счете на тысячи вряд ли стоит запускать их все). Сойдемся на мысли, что тесты должны быть настолько быстрыми, чтобы вы, как разработчик, чувствовали себя комфортно.

Независимость

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

Повторяемость

Мысль проста до безумия: если прогон одного и того же теста с одинаковыми (относительно условий теста) параметрами приводит к разному результату, то с тестом надо прощаться. Разработчики склонны доверять тестам и ищут в них ошибку в последнюю очередь, перевернув с ног на голову весь функциональный код. Нет ничего более раздражающего, чем потратить гору времени на поиск ошибки из-за упавшего теста, а потом обнаружить, что с функциональным кодом все Ок и все дело в "плавающей" ошибке в тестовом коде.

Самодостаточность

Автотест самостоятелен и сам способен проверить прошел он или нет. Не должно быть ручных проверок данных, изучения консоли и прочего. Все, что нас интересует - цвет теста (красный/зеленый), сигнализирующий о двух возможных состояниях. Любое отступление от этой идеи похоронит идею автотестирования в любом варианте промышленной разработки ПО.

Тщательность

Общая мысль сводится к тому, что зеленый тест должен означать действительную работу системы, без оговорок. Если тест проверяет валидность номера телефона, то должны быть протестированы все варианты ввода, оговоренные правилами. Если функциональный код не должен допустить в поданных строке пробелов, то тест должен проверить все варианты размещения пробела - в начале, в середине и в конце. В ином случае толку от десятков и сотен тестов не будет - баги будут по-прежнему сыпаться отовсюду. И нет, я не говорю про сакраментальное 100% покрытие кода. Все сводится к тому, что тест должен со 100% уверенностью говорить о работоспособности метода, который он проверяет.

Время написания

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

Читаемость

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

Структура

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

Именование

В случае с именами тестовых методов стоит отойти от привычных правил хорошего тона) Здесь можно использовать и три и пять и семь слов в названии (обычно разделяемых "_"), лишь бы после его прочтения не оставалось сомнений в назначении теста.

Размер имеет значение

Вспоминаем канонические 10-15 строчек на метод из книг Макконнела и Мартина. И тут основная причина даже не в читаемости или нарушении структуры, длинный тест означает, что у него либо чересчур много ответственности, либо в нем затесалась логика; от таких вещей тесты умирают, при том быстро, но мучительно для разработчика)

Атомарность

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

  • когда тест ломается, определение того, что конкретно сломано занимает секунды, а не требует перечитывать код и дебажить код, а то и сам тест для поиска проблемы

  • когда функционал меняется, мы правим/удаляем конкретный тест, а не разбираемся с логикой длинного многофакторного монстра, рискуя что-нибудь поломать.

Отсутствие логики

Никогда и ни при каких обстоятельствах тест не должен заниматься расчётами и повторением функционального кода. Если нужно убедиться, что некий метод переворачивает строку, создайте два string'a - input/output - и присвойте им соответствующие значения. Попытка внутри теста перевернуть строку и сравнить с ней результат работы функционального кода черевата очень большими проблемами:

  • не забудем ли мы какие-то условия из функционального кода? а если мы так хорошо расписали весь функционал внутри теста, то не пора ли писать тест на тест?)

  • при изменении логики функционального теста придется переписывать и логику теста.

  • мы неизбежно нарушим правила про читаемость и лаконичность.

Минимум утверждений

Идеальный тест содержит только одно утверждение (assert). На деле, конечно, не всегда получается выдержать это условие и принято считать нормальным наличие до трех assert'ов, не забывая при этом, что они должны быть логически связаны. Так, если мы рассчитываем получить в ответе, скажем, список string'ов, содержащий только одну конкретную запись(например, "example"), то можно написать

    Assert.True(list.Count == 1);
    Assert.True(list.First() == "example");

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

P.S.

Нарушение описанных идей неизбежно приводит к проявлению одной или нескольких болезней... Тесты слишком хрупкие и ломаются при каждом чихе на функциональный код, разработчикам в конце концов надоест заниматься бесконечными починками и идея с автономным тестированием будет пущена под откос. Тесты сложно поддерживать. Никто не может понять, что написано и что делать в случае, если тест не прошел. И снова затраты времени и нервов на чтение и правку приведут к фатальному для тестирующего проекта финалу. Ломающиеся время от времени тесты - что тот мальчик с криками "Волки! Волки!". В какой-то момент все просто перестанут обращать внимание на падения. Длительное время прогона тестов ведет к тому, что разработчики будут запускать их как можно реже, финал понятен) Можно еще накинуть пару-тройку сценариев, но, imho, мысль понята - отступление от этих канонов грозит смертью самой идее автотестирования и если уж мы встали на славную дорогу unit-тестирования, то должны всеми силами стараться не допустить этого)