View on GitHub

wiki

Technical Excellence Wiki

Описательное тестирование

Создаем тесты для описания и исправления существующего кода

Перевод статьи на русский язык Майкла Физерса Characterization Testing

Я думаю что наиболее запутывающая вещь в тестировании это само слово тестирование. Мы используем его для многих вещей, от исследовательского (exploratory) И “ручного” тестирования до модульного (unit) и других типов автотестов. Основная проблема в том, что нам необходимо понимать что наш код работает и мы свалили в кучу различные практики под этим знаменем. Можно поискать какие-то обычные различия между ними, но их тяжело найти.

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

А что если это не так?

К несчастью, это относится ко множеству программного обеспечения. Нам нужно производить изменения в нем, но у нас нет достаточного понимания о том, что оно делает.

Давайте взглянем на небольшой пример.

public static String formatText(String text) 
{
  StringBuffer result = new StringBuffer();
  for (int n = 0; n < text.length(); ++n) {
    int c = text.charAt(n);
    if (c == '<') {
      while(n < text.length() && text.charAt(n) != '/' && text.charAt(n) != '>')
        n++;
      if (n < text.length() && text.charAt(n) == '/')
        n+=4;
      else
        n++;
    }
    if (n < text.length())
      result.append(text.charAt(n));
  }
  return new String(result);
}

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

Начнем с простого теста.

    @Test
    public void formatsPlainText() {
        assertEquals("plain text", Pattern.formatText("plain text"));
    }

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

Давайте вернемся и проделает это ещё раз.

Я собираюсь написать тест и назвать его x. Я называю его x потому что я не знаю что функция formatText сделает. Я даже не задам настоящее предполагаемое значение потому, что в этой точке я не знаю какое будет поведение.

    @Test
    public void x() {
        assertEquals(null, Pattern.formatText("plain text"));
    }

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

    java.lang.AssertionError: expected:<null> but was:<plain text>
            at org.junit.Assert.fail(Assert.java:88)
            ...

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

    @Test
    public void doesNotChangeUntaggedText() {
        assertEquals("plain text", Pattern.formatText("plain text"));
    }

Сложно отвлечься от мысли что это в каком-то виде жульничество. Это слишком просто. Мы полностью опустили размышление об ожиданиях и мы пишем тест. Какую ценность мы можем извлечь из этого?

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

Вот тест для другого аспекта поведения функции. Он проходит. Показывает ли он нам наличие бага в коде?

    @Test
    public void removesTextBetweenAngleBracketPairs() {
        assertEquals("", Pattern.formatText("<>")); 
    }

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

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

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

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

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

Начните писать тест с названием x

Перевел: Сергей Лобин

Остались вопросы? Задавай в нашем чате.