четверг, 3 июня 2010 г.

Пять принципов чистых тестов (F.I.R.S.T. Principles)

Многим разработчикам известны принципы проектирования, которые благодаря Роберту Мартину получили звучное название  S.O.L.I.D. Многим из этих принципов уже не один десяток лет (принцип подстановки Лисков впервые был озвучен более двадцати лет назад), они были опробованы на миллионах строк кода, тысячами разработчиками (при этом добрая половина из них применяла эти принципы даже не имея понятия об этом:) ). Конечно, слепое следование любым принципам никогда ни к чему хорошему не приводило и приводить не будет, но тем не менее в них описаны разумные вещи, о которых нужно как минимум знать, да и понимать их тоже будет совсем не лишним.

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

Принципы написания качественных тестов придуманы не на пустом месте. Большинство опытных разработчиках знают о них точно так же, как и о принципах проектирования без помощи “Дядюшки” Боба, но как и в случае с принципами проектирования именно Боб Мартин объединил пять принципов тестирования, в результате чего получилось звучное название Б.Н.П.О.С. (или F.I.R.S.T., если вам звучность русскоязычного названия не по душе): Fast (Быстрота), Independent (Независимость), Repeatable (Повторяемость), Self-Validating (Очевидность) (*), Timely (Своевременность)).

Итак, каждый модульный тест должен обладать следующими характеристиками:

Быстрота (Fast). Тесты должны выполняться быстро. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы. А ленивый человек не захочет запускать тесты при каждом изменении кода, если они будут долго выполняться.

Независимость (Independent). Результаты выполнения одного теста не должны быть входными данными для другого. Все тесты должны выполняться в произвольном порядке, поскольку в противном случае при сбое одного теста каскадно “накроется” выполнение целой группы тестов.

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

Очевидность (Self-Validating). Результатом выполнения теста должно быть булево значение. Тест либо прошел, либо не прошел и это должно быть легко понятно любому разработчику.  Не нужно заставлять людей читать логи только для того, чтобы определить прошел тест успешно или нет.

Своевременность (Timely). Тесты должны создаваться своевременно. Несвоевременность написания тестов является главной причиной того, что они откладываются на потом, а это “потом” так никогда и не наступает. Даже если вы и не будете писать тесты перед кодом (хотя этот вариант уже доказал свою жизнеспособность) их нужно писать как минимум параллельно с кодом.

-----------------------------------

(*) Пять этих принципов опубликованы в книге “Clean Code”, поэтому русскоязычный вариант перевода взят из русскоязычного издания этой книги. Можно долго спорить о корректности перевода Self-Validating, как “Очевидность”, но, как говорится, что написано пером… И хотя вариант типа “Самодостоверность” выглядит более корректным, с точки зрения семантики вариант “Очевидность” кажется не таким уж и плохим.

8 комментариев:

  1. А действительно ли важна быстрота? На более-менее большом проекте тестов должно быть много. Необходимость запускать их все при каждой компиляции уменьшается с ростом количества тестов :о))

    А если запускать их всеря-от-времени, то скорость получается уже не так важна.

    На мой взгляд, именно по причине увеличения количества тесто в течении времени, скорость выполнения тестов становится важной величиной: ибо когда тесты выполниются восемь\десять часов, то своевременно реагировать на их поломку становится сложно :о))

    ОтветитьУдалить
  2. @_FRED_: Здесь речь идет о характеристике одного единственного теста, а не множества всех возможных тестов всей системы.

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

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

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Все мы знаем, что разработчики люди, а люди ленивы, поскольку эти выражения являются “транзитивными”, то можно сделать вывод, что люди тоже ленивы.

    Все правильно?)

    Результаты выполнения одного теста не должны быть входными данными для другого. Все тесты должны выполняться в произвольном порядке, поскольку в противном случае при сбое одного теста каскадно “накроется” выполнение целой группы тестов.

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

    ОтветитьУдалить
  5. @Роман: не совсем понял вашу мысль.
    Я имел ввиду то, что классические модульные тесты должны быть независимые (именно модульные) поскольку в противном случае этого уже не модульный тест, а нечто иное.
    Про другие же тесты здесь речь не велась.

    ОтветитьУдалить
  6. _FRED_ пишет...
    А действительно ли важна быстрота? ...
    А если запускать их всеря-от-времени, то скорость получается уже не так важна.

    Быстрота модульных тестов очень важна - я считаю, что они должны запускаться как можно чаще, хотя бы перед каждым коммитом в хранилище. И вряд ли кто-то будет делать это, если они будут выполняться десять минут - должно быть не больше минуты, если количество тестов не превышает где-то 5000. Больше времени будут занимать тесты, которые работают непосредственно с UI, но их можно вынести в отдельную категорию и запускать "по праздникам".

    ОтветитьУдалить
  7. Тут прошла мысль по поводу модульных тестов с UI. Вопрос - а кто-то так делает? Я очень предвзято отношусь к модульному тестированию UI. На моей памяти это всегда печально заканчивалось. Я считаю, что тестирования UI - ни разу не модульное. Тесты с кодом var form = new Form - аццкое зло и гореть им в аду. А если это нет (ну и пожалуй реальной работы с БД), то и тесты будут проходить быстро.

    ОтветитьУдалить
  8. @eugene: вся UI-related логика все равно должна лежать не в формах и тестироваться отдельно. В таком случае, все, что остается в форме - это код привязки данных и еще какая-то мелочь.

    Я как-то пробовал покрыть тестами привязку данных, но потом забил, поскольку затраты не оправдываются.

    Да, юнит-тесты UI-я (ты прав, это уже не совсем юнит-тесты) - это IMHO оверкил.

    З.Ы. Не понял связи тестов форм и времени. У меня формы тестировались довольно шустро с точки зрения времени выполнения, просто выхлоп был маленький.

    ОтветитьУдалить