Pull to refresh

Тесты разные важны, а иные не нужны

Reading time 5 min
Views 2.9K
Зачем писать тесты? Когда их писать? Когда их не писать? Хабр был и остается неравнодушен к этим и другим вечным вопросам, и не один хабровчанин провел бессонную ночь, придумывая убедительные на них ответы (чтобы оппоненты, наконец, заткнулись).

Я решил взять противника не умением, а числом, и поэтому вот вам 19 причин, по которым писать тесты все-таки стоит (в хронологическом порядке). Итак, тесты
  1. Помогают разработать API создаваемого класса. Вместо того, чтобы выдумывать интерфейс класса, вы вырабатываете его в процессе написания теста.
  2. Помогают разработать архитектуру приложения. Как минимум, это архитектура на низком уровне — способ взаимодействия классов друг с другом. Как показывает опыт, такая архитектура, как правило, оказывается наиболее гибкой и экономичной.
  3. Проверяют, работает ли определенный кусок кода прямо сейчас.
  4. Проверяют, будет ли работать ли определенный кусок кода после внесенных изменений.
  5. Документируют функционал отдельных классов.
  6. Документируют поведение системы (с точки зрения пользователя).
  7. Больше пока не придумал, может, кто-нибудь подскажет?

Понятно, что тест тесту рознь, и далеко не всякие тесты способны принести такую огромную пользу. Давайте попробуем разобраться.

Пишем тесты до кода или после?


Тут все, более или менее, понятно. Когда мы пишем тест после кода, мы теряем бесценную возможность позволить тестам помочь нам в написании кода. На мой взгляд, это самая большая ценность тестов. Однако, и самая трудная для понимания, ибо тут требуется заткнуть внутреннего Архитектора и дать тестам порулить.

Второй немаловажный момент: как утверждают многоопытные, тесты, отложенные на потом, в большинстве случаев так и остаются ненаписанными.

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

Какой кусок кода тестировать?


Здесь нет «или-или», а, скорее, непрерывный спектр: от метода в одну строчку до целого сценария, включающего в себя последовательность действий пользователя. Первая крайность называется модульным (unit), вторая — приемочным (acceptance) тестированием, а где-то в промежутке располагаются интеграционное (кто-нибудь знает слово получше?) и функциональное тестирование, причем каждый понимает под этими терминами что-то свое. В народе модульное тестирование значительно более популярно, и, когда речь идет о Test Driven Development, по умолчанию подразумеваются юнит тесты. Во-первых, их значительно легче писать — в смысле, это требует меньше кода, а во-вторых, это связано с тем, что девелоперы любят писать девелоперские тесты (о них позже).

Посмотрим теперь, как такие и сякие тесты соотносятся с нашим списком Благих вещей:

Юнит тесты:
  1. Помогают разработать API создаваемого класса.
  2. Помогают разработать архитектуру приложения.
  3. Проверяют, работает ли определенный кусок кода прямо сейчас.
  4. Проверяют, будет ли работать ли определенный кусок кода после внесенных изменений.
  5. Документируют функционал отдельных классов.


Как видим, благ огромное количество, нехватает лишь одного (самого главного) — проверки корректности работы всей системы, или, хотя бы, какого-то осмысленного ее куска. Как совокупность здоровых нейронов может породить больной мозг, так и набор правильно работающих классов еще не гарантирует нормальную работу системы. Даже если мы протестируем взаимодействие классов друг с другом, модульный тест лишь даст нам гарантию, что данный класс посылает некие конкретные сигналы своим соседям. А вот нужны ли эти сигналы всеобщей гармонии, модульный тест нам не скажет.

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

Помогает ли интеграционный тест в разработке дизайна приложения? Сам по себе нет, он проверяет огромный кусок кода, и сам по себе этот кусок может быть организован совершенно безобразно. Но если мы начнем с интеграционного теста, а потом, в процессе рефакторинга, будем дописывать модульные тесты на отдельные участки кода, то это принесет поистине бесчисленные блага.

Что нам, вообще, тестировать?


Большинство разработчиков даст на этот вопрос очевидный и, прямо скажем, весьма тупорылый простодушный ответ: будем тестировать классы и их методы. Сказано — сделано. На каждый класс, который выдумала наша гениальная башка, мы заводим тестовый класс. Например, если у нас есть MyClass, мы бесхитростно заводим класс MyClassTester. Далее, поскольку мы уже совершенно точно знаем, что у нас есть метод DoSomething, мы заводим метод TestDoSomething. И, что самое интересное, спокойно спим после этого по ночам.

Говорят, в Visual Studio есть даже такая команда, которая все это дело генерит автоматически.

Если же мы спросим заказчика, ну, или даже хотя бы к примеру вот тестера, то будем иметь другой весьма очевидный ответ: тестировать будем поведение системы. У нас есть спецификация, или там пользовательские истории, берем и переписываем их в исполняемом виде. И вот это уже работа для мозга: именно в процессе написания таких тестов и начинается этот священный для многих присутствующих процесс под названием Test Driven Design.

Первая разновидность тестов иногда называется девелоперскими, а вторая — пользовательскими. Есть тенденция путать это разделение на модульные и интеграционные тесты. Со всей ответственностью заявляю, что это не так. Иногда превратить девелоперские тесты в пользовательские можно простым переименованием и перегруппировкой тестовых методов. Типичный пример — поиск. У нас может быть один класс, ответственный за отбор данных по нескольким параметрам. В случае девелоперских тестов, у нас один тестовый класс с кучей тестов. Разобьем его на классы по сценариям: поиск по имени, по дате и т.д. Мы получили пользовательские тесты. Как их писать и организовывать — не совсем очевидно, но польза от них огромная. Помимо нижеперечисленного, мы получаем систему, в которой нет избыточного тестирования. Это значит, что мы можем менять реализацию функционала без опасений, что упадут некоторые тесты (а именно те, которые тестируют не фичи, а способ их реализации).

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


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


То есть, если я хочу понять, что делает вот этот вот класс, мне придется смотреть на код самого класса. Чтобы понять пример его использования, я погляжу на код девелоперского теста. А вот чтобы понять, как система делает то или другое полезное дело, я буду смотреть на код пользовательского теста.

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

Заключительные мысли


Ничего заключительного я так и не придумал, поэтому оставляю все, как есть.
Tags:
Hubs:
+27
Comments 37
Comments Comments 37

Articles