Вы здесь: Home > Статьи > Учимся любить юнит тесты
Что здесь происходит
Правила XP
Статьи по XP
Книги по XP
Ссылки по XP
Обсудить
Написать нам

Учимся любить юнит тесты

Перевод статьи из журнала STQE, PDF версия - на сайте Pragmatic Programmer.

Улучшение разработки через написание тестов до кодирования.

Дейв Томас и Энди Хант

Почему многие разработчики не используют юнит тесты? В конце концов, юнит тесты помогают производить хорошо спроектированные системы и более аккуратный код. Недавний успех экстремального программирования и системы тестирования xUnit Гамма и Бека, заставил говорить о юнит тестах многих кодировщиков. Но все еще множество (возможно, большинство) программистов избегают писать их. Эта статья является попыткой переубедить этих разработчиков.

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

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

Что дает нам право говорить об этом? Мы - программисты, которые испробовали все плохие оправдания, приведенные здесь. Но теперь мы знаем лучше... честно.

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

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

Немного Потестировать, Немного Покодировать

Разработчица Анна  смотрит на доску, нет ли следующей задачи. Похоже, серверным ребятам нужен класс, который манипулирует именами файлов. В первую очередь нужен метод, который будет генерировать имя файла с определенным расширением на основании заданного имени. Передайте ему "fred" или "fred.xml" и он сгенерирует "fred.txt" или "fred.bak". Не особенно сложно, думает она. Поскольку Анна программирует на Java, и так, как это первая функция манипулирования именами файлов, она решает создать новый package для этого кода. Она создает директорию для кода package, затем делает в ней поддиректорию для тестов. Теперь ей надо написать код.

Анна выработала привычку - когда она создает новый класс, она начинает писать минимальный исходный код. Все что он делает - декларирует package для ее кода, и определяет пустой класс (в данном случае, Name Mangler). Она добавит детали позже. Хотя следующий код и специфичен для Java, привычки Анны писать юнит тесты являются хорошим общим примером.

package com.mycompany.server.fileutil;
public class NameMangler {

}

Затем она идет в поддиректорию "test" и пишет начало юнит теста. Она всегда пишет базовый smoke тест сначала, просто создавая объект тестируемого класса.

package com.mycompany.server.fileutil.test;
import junit.framework.*;
import com.mycompany.server.fileutil.*;

public class TestNameMangler extends TestCase {
  // … standard setup code …
  
  // Test object creation
  public void test_smoke() {
    NameMangler n = new NameMangler();
  }
}

Поскольку Анна использует систему JUnit, она может написать Java класс, содержащий несколько тестовых методов. В нашем случае, Анна делает имя тестового класса (TestNameMangler) отражающим имя тестируемого класса. Он содержит единственный (пока) метод, test_smoke.

Ну, достаточно кодировать, надо посмотреть, работает ли это. Анна использует утилиту Ant для того, чтобы собирать свои проекты, поэтому она набирает "ant test" в командной строке. Ее два файла компилируются и все юнит тесты приложения, включая новые, запускаются. Все работает.

За что же ей все это? Еще не написано ни строчки реального кода, а она уже должна прогонять все тесты для целой системы. Ну, Анна находит это полезным по нескольким причинам. Во-первых, это служит быстрой проверкой на разумность. (Правильно ли она задала директории и названия Package?) Во-вторых, это подтверждает, что тесты выполняются по отношению к коду, который они должны тестировать. В-третьих, это проверка того, что она случайно не сделала ничего плохого остальным частям системы. И, наконец, это пролом льда. Как программисту, ей иногда страшно начинать кодировать, глядя на пустой буфер редактора. Написание этих нескольких строк проводит нас через этот барьер. Теперь мы просто добавляем что-то к имеющемуся коду.

Анна продолжает работу над своей задачей. Ее класс имеет конструктор, которому передают имя файла (с расширением или без него) как параметр. Этот класс также предоставляет метод change_ext, возвращающий новое имя файла с другим расширением. Первое, что она делает - изменяет smoke тест (передает имя файла "fred" в конструктор). Пока она здесь, она пишет другой тестовый метод, который исследует результат метод change_ext (который еще надо будет написать). Тест передаст NameMangler-у имя файла без расширения и проверит, правильно ли добавилось расширение.

public void test_with_no_ext() {
  // Create a new NameMangler to test
  NameMangler n = new NameMangler("fred");
  // and make sure it adds ".txt" to the base name
  assertEquals("fred.txt", n.change_ext("txt"));
}

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

Итак, теперь, когда у нее есть несколько неработающих тестов, она пишет код класса NameMangler, чтобы починить их. Она добавляет параметр к конструктору и создает метод change_ext:

public NameMangler(String name) {
  this.name = name;
}

public String change_ext(String new_ext) {
  return name + "." + new_ext;
}

Метод change_ext, написанный Анной, тривиальный (все, что он делает - добавляет расширение к имени файла). Это все, что ей нужно, чтобы прошли имеющиеся тесты, поэтому это все, что она сделала. Она прогоняет тесты, и все проходит. Она не может подавить улыбку - проходящие тесты - это настоящая психологическая поддержка, даже тогда, когда тесты такие простые.

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

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

public void test_change_existing() {
  NameMangler n = new NameMangler("fred.xml");
  assertEquals("fred.txt", n.change_ext("txt"));
}

Она запускает тесты и они не проходят - ее текущая реализация не обрезает имеющееся расширение перед тем, как добавить новое. Она пишет еще немного кода:

public String change_ext(String new_ext) {
  String old = name;
  int dot_pos = old.indexOf('.');
  if (dot_pos > 0)
    old = old.substring(0, dot_pos – 1);
  return old + "." + new_ext;
}

Ее уверенная улыбка блекнет, когда она прогоняет тесты. Тест падает в assertion в test_change_existing, утверждая, что процедура вернула "fre.txt" вместо "fred.txt". Она быстро находит ошибочный "-1" и удаляет его. Тест проходит. Она отхлебывает кофе, обдумывая написание следующего теста. Оставим ее пока.

Так почему же не все тестируют?

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

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

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

Как же убедить разработчиков в том, что тестирование экономит время? Просто почитать об этом не помогает. Факты давно известны, но в поведении разработчиков мало что меняется. Единственный работающий путь, который мы нашли - сделать так, чтобы разработчики сами попробовали тестирование. Если вы разработчик, покажите другим, как просто писать ваши тесты и как они сохраняют ваше время и уменьшают стресс. Предложите создать заглушки для юнит тестов их кода, помогите им преодолеть начальные затруднения. Если вы тестировщик, вы можете напомнить разработчикам что отчеты об ошибках, которые вы им направляете - это отличная основа для новых юнит тестов. В конце концов, они должны протестировать все, что ломается до того, как они начнут исправлять ошибку. Иначе, как они могут быть уверены, что ошибка исправлена? И если они не знают наверняка, что ошибка исправлена, то они рискуют повторить цикл тестирования и исправления еще раз, потратив впустую время всех участников.

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

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

Трудность появляется из-за того, что это не то, как программистов учили программировать. Это выглядит ненатурально (сначала). Этот дискомфорт усиливает чувство того, что тесты отвлекают.

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

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

Но даже самые лучшие разработчики делают ошибки. Фактически, лучшие разработчики, скорее всего, несут ответственность за самые хитрые ошибки. Как только такие ошибки попадают в конечное приложение, они имеют возможность взаимодействовать с другими ошибками, делая диагностику очень сложной. Посмотрите на простейшую ошибку Анны. Ее код нормально работал, он просто выдавал немного не такой ответ. Если этот ошибочный код интегрировали в приложение, то это могло бы привести к потере файлов, или переписыванию существующих файлов. Другие разработчики бы тратили время, разбираясь с тем, что хотя это их код генерировал эти неправильные файлы, ошибка была не у них.

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

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

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

Сравните это со стилем работы Анны. Она, возможно, ничуть не лучше технически, но она постоянно контролирует то, что она делает. Она двигается небольшими шагами, и всегда знает, когда она закончила. Ее работа всегда идет по одному и тому же пути - принять решение по функциональности, написать тест, написать код, чтобы тест проходил - поэтому, она никогда не ощущает себя потерявшейся.

Стиль тестирования Анны делает жизнь проще независимо от способностей к кодированию. Простота означает контроль, который ведет к лучшему коду.

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

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

Бывает код, который нельзя протестировать
Проведя некоторое время в борьбе с оправданиями, чтобы не делать юнит тесты, мы должны покаяться. Это одно оправдание, которое имеет основу. Что могут сделать разработчики, если они общаются с железом, или внешней системой, которая не контролируется ими? Иногда эти внешние компоненты просто не будут нормально работать во время тестирования. Они могут быть слишком медленными, или возвращать непредсказуемые результаты при каждом вызове (зачастую с полным основанием - котировщик акций, выдающий одну и ту же цену при каждом вызове, облегчит тестирование, но посеет подозрения на бирже). Чтобы это преодолеть, разработчики могли бы написать тестовую оболочку, которая симулирует эти компоненты. Но для более сложных интерфейсов, вы должны взвесить затраты и отдачу. Можно держать пари, что NASA подробно тестирует свои программы и железо, но их бюджет, скорее всего, превышает ваш.

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

UI - это особый случай. Существуют технологии, позволяющие выполнить юнит тест прямо на уровне экрана, клавиатуры и мыши, но они обычно неудобны. Мы советуем разрабатывать тонкий уровень UI с хорошо определенными интерфейсами к остальной части приложения. Тестируйте юнит тестами вплоть до этого уровня и оставьте GUI для тестировщиков.

Преимущества помимо тестирования

Многие разработчики не понимают, что юнит тестирование дает намного больше, чем просто тесты.

Во-первых, тесты - это не инструмент для того, чтобы правильно написать код. Тесты - это  среда, в которой проходит только правильный код (или, настолько правильный, насколько точны тесты). Тесты - это проверка на разумность и страховочная сеть, которая постоянно в вашем распоряжении. Комик Стивен Райт говорит: "Знаете, как это когда вы качаетесь в кресле так далеко назад, что чуть не падаете? Вот так я чувствую себя постоянно." Мы подозреваем, что многие кодировщики разделяют это чувство нестабильности и неминуемой опасности всякий раз, когда они изменяют код. Юнит тесты лечат это - вы знаете, что вы не разломали систему, потому что все тесты в системе проходят.

Во-вторых, тесты влияют на дизайн вашего кода. Когда разработчики привыкают писать на все юнит тесты, то они обнаруживают, что некоторые вещи, которые они пишут, трудно тестировать. Иногда они замечают, что пишут навороченную систему для того, чтобы протестировать один метод. Иногда они обнаруживают, что метод сложно тестировать потому, что он обслуживает слишком много разнообразных случаев. И иногда что-то сложно протестировать потому, что код, который делает необходимую работу, погребен под слоями другого кода. Разве все это причины, чтобы не тестировать? Наоборот, такие открытия - наиболее ценные результаты тестирования, потому что тесты рассказывают вам секреты о структуре кода. Прислушайтесь к тестам и рефакторите (модный вариант термина реструктуриррвать) соответственно. Код будет лучше после этого. Методы будут выполнять ясно определенные функции. Иерархии классов будут более плоские, и они будут лучше отражать бизнес смысл приложения. Будет меньше зависимостей между классами. Все это облегчает написание кода и делает сопровождение проще.

В-третьих, тесты уменьшают панику. Паника - злейший враг разработчика, в частности, когда вам надо выпустить продукт через неделю, а ваша система выглядит не стабильнее голливудской семейной пары. Исправление каждой ошибки ломает три других вещи и ошибки, которые, вы можете поклясться, исправили два дня назад, вдруг снова появляются. Однако, вы можете прервать эту череду проблем. Как? Перестаньте исправлять ошибки, и начните писать тесты. Вы обнаружите нечто удивительное. Тесты найдут множество маленьких проблем в коде, о котором вы думали, что он правильный. И по мере исправления каждой маленькой проблемы, общая стабильность системы будет улучшаться. Множество мелких ошибок ведут к хаотическому поведению системы, и простейший путь находить их - по одной.

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

Любая практика, которая делает разработку более аккуратной, легче в сопровождении и понимании, а также более веселой, заслуживает того, чтобы ее попробовать. Так что, если вы разработчик, скачайте копию JUnit (или системы тестирования для языка, который вы используете) и начните писать тесты для своего текущего проекта.

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

Юнит тесты похожи на это. Некоторое время вы их делаете с сомнением. Они работают, но чего особенного? Затем внезапно вы понимаете, что тестирование стало частью вашего стиля кодирования - оно впиталось в вашу кровь и превратилось в привычку. Вы стали (как сказали бы Кент Бек и Эрих Гамма) зараженными тестированием. Лекарства нет.


Дейв Томас и Энди Хант - партнеры в The Pragmatic Programmers - консалтинговой фирме, специализирующейся на agile методах разработки и тренингах. Они авторы книг The Pragmatic Programmer и Programming Ruby и одни из основателей Agile Alliance. Их философия в том, что компетенция и отношение каждого разработчика является самым важным компонентом успешного проекта. Они читают лекции и курсы, основанные на этой идее по всему миру.