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

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

Если вы так думаете, я думаю, вы не понимаете, что такое TDD на самом деле. (Хорошо, предыдущее предложение должно было привлечь ваше внимание). По TDD есть очень хорошая книга Разработка через тестирование: на примере, Кент Бек, если вы хотите ее проверить и узнать больше.

В этой статье я рассмотрю основы разработки через тестирование и рассмотрю распространенные заблуждения о технике TDD. Эта статья также является первой из ряда статей, которые я собираюсь опубликовать, о разработке через тестирование.

Зачем использовать TDD?

Есть исследования, статьи и дискуссии о том, насколько эффективна TDD. Хотя иметь некоторые цифры определенно полезно, я не думаю, что они отвечают на вопрос, почему мы должны вообще использовать TDD.

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

Но приведенное выше соображение касается тестирования, а не самого TDD. Так почему именно TDD? Короткий ответ: «потому что это самый простой способ добиться как хорошего качества кода, так и хорошего тестового покрытия».

Более подробный ответ исходит из того, что на самом деле представляет собой TDD… Начнем с правил.

Правила игры

Дядя Боб описывает TDD тремя правилами:

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

- Вам не разрешается писать больше модульного теста, чем достаточно для отказа; а сбои компиляции - это сбои.

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

Еще мне нравится более короткая версия, которую я нашел здесь:

- Напишите только единичный тест, чтобы он не прошел.

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

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

Эти правила определяют механику TDD, но это определенно не все, что вам нужно знать. Фактически, процесс использования TDD часто описывается как цикл «красный / зеленый / рефакторинг». Посмотрим, о чем это.

Красный Зеленый Цикл рефакторинга

Красная фаза

На красной фазе вы должны написать тест на поведение, которое вы собираетесь реализовать. Да, я написал поведение. Слово «тест» в разработке через тестирование вводит в заблуждение. В первую очередь мы должны были назвать это «Развитие, управляемое поведением». Да, я знаю, некоторые люди утверждают, что BDD отличается от TDD, но я не знаю, согласен ли я с этим. Итак, в моем упрощенном определении BDD = TDD.

Здесь возникает одно распространенное заблуждение: «Сначала я пишу класс и метод (но без реализации), затем я пишу тест для проверки этого метода класса». На самом деле это не так.

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

Каждая фаза R.G.R. Цикл представляет собой этап жизненного цикла кода и то, как вы можете к нему относиться.

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

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

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

Давайте посмотрим на пример.

// LeapYear.spec.js
describe('Leap year calculator', () => {
  it('should consider 1996 as leap', () => {
    expect(LeapYear.isLeap(1996)).toBe(true);
  });
});

Приведенный выше код является примером того, как тест может выглядеть в JavaScript с использованием среды тестирования Jasmine. Вам не нужно знать Жасмин - достаточно понять, что it(...) - это тест, а expect(...).toBe(...) - это способ заставить Жасмин проверить, все ли идет так, как ожидалось.

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

Этот тест на самом деле имеет некоторые последствия:

  • Название калькулятора високосного года - LeapYear.
  • isLeap(...)это статический метод LeapYear
  • isLeap(...) принимает в качестве аргумента число (а не массив, например) и возвращает true или false.

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

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

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

А что насчет абстракции? Увидим это позже, на этапе рефакторинга.

Зеленая фаза

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

Здесь возникает еще одна большая ошибка: вместо того, чтобы писать достаточно кода, чтобы пройти красный тест, вы пишете все алгоритмы. Делая это, вы, вероятно, думаете о том, какая реализация будет наиболее эффективной. Ни за что!

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

Но почему у нас это правило? Почему я не могу написать весь код, который уже находится в моей голове? По двум причинам:

  • Простая задача менее подвержена ошибкам, и вы хотите минимизировать количество ошибок.
  • Вы определенно не хотите смешивать код, который проходит тестирование, с кодом, который не тестируется. Вы можете написать код, который не тестируется (он же устаревший), но худшее, что вы можете сделать, - это смешать проверенный и непроверенный код.

А как насчет чистого кода? А как насчет производительности? Что, если написание кода заставит меня обнаружить проблему? А как насчет сомнений?

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

Методика разработки, основанная на тестировании, предусматривает еще две вещи: список дел и этап рефакторинга.

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

Feature: Every year that is exactly divisible by four is a leap year, except for years that are exactly divisible by 100, but these centurial years are leap years if they are exactly divisible by 400.
- divisible by 4
- but not by 100
- years divisible by 400 are leap anyway
What about leap years in Julian calendar? And years before Julian calendar?

Список дел активен: он меняется во время кодирования и, в идеале, в конце реализации функции он будет пустым.

Фаза рефакторинга

На этапе рефакторинга вам разрешено изменять код, сохраняя при этом все тесты зелеными, чтобы он стал лучше. Что значит «лучше» - решать вам. Но есть кое-что обязательное: нужно убрать дублирование кода. Кент Бекс предполагает в своей книге, что удаление дублирования кода - это все, что вам нужно сделать.

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

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

Например, следующий код:

class Hello {
  greet() {
    return new Promise((resolve) => {
      setTimeout(()=>resolve('Hello'), 100);
    });
  }
}

class Random {
  toss() {
    return new Promise((resolve) => {
      setTimeout(()=>resolve(Math.random()), 200);
    });
  }
}

new Hello().greet().then(result => console.log(result));
new Random().toss().then(result => console.log(result));

может быть преобразован в:

class Hello {
  greet() {
    return PromiseHelper.timeout(100).then(() => 'hello');
  }
}

class Random {
  toss() {
    return PromiseHelper.timeout(200).then(() => Math.random());
  }
}

class PromiseHelper {
  static timeout(delay) {
    return new Promise(resolve => setTimeout(resolve, delay));
  }
}

const logResult = result => console.log(result);
new Hello().greet().then(logResult);
new Random().toss().then(logResult);

Как видите, для устранения дублирования кода new Promise и setTimeout я создал метод PromiseHelper.timeout(delay), который обслуживает классы Hello и Random.

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

Заключительные соображения

В этом разделе я постараюсь ответить на некоторые распространенные вопросы и заблуждения о разработке Test Drive.

  • Т.Д.Д. требует гораздо больше времени, чем «обычное» программирование!

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

  • Сколько тестов мне нужно написать?

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

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

Это не может быть более ложным. Если то, что вы собираетесь реализовать, не очень хорошо продумано, в какой-то момент вы подумаете: «Ой! Я не считал… ». А это значит, что вам придется удалить производственный и тестовый код. Это правда, что TDD помогает с рекомендацией «Достаточно, как раз вовремя» для гибких методов, но это определенно не подменяет фазу анализа / проектирования.

  • Должно ли тестовое покрытие быть 100%?

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

  • Я могу писать код с очень небольшим количеством ошибок, мне не нужно тестирование.

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

  • TDD хорошо работает на примерах, но в реальном приложении большая часть кода не тестируется.

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

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

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

Что дальше?

Эта статья была о философии и распространенных заблуждениях TDD. Я планирую написать другие статьи о TDD, где вы увидите много кода и меньше слов. Если вам интересно, как разработать тетрис с помощью TDD, следите за обновлениями!