Разработка, ориентированная на тестирование – что это такое, а что нет

1656587060 razrabotka orientirovannaya na testirovanie – chto eto takoe a chto

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

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

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

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

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

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

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

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

Долгий ответ вытекает из того, что такое TDD… Давайте начнем с правил.

Правила игры

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

— Вам запрещено писать любой производственный код, если он не имеет целью пройти неудачный модульный тест.

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

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

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

– Напишите достаточно модульного теста, чтобы он провалился.

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

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

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

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

JN8oQMbYFGuFdXSJJEybTQ8rCFwVclmyRANN
Красный зеленый рефактор

Красная фаза

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

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

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

Каждая фаза цикла RGR представляет фазу жизненного цикла кода и то, как вы можете к нему относиться.

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

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

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

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

// LeapYear.spec.jsdescribe('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 дублирование кода, я создал a PromiseHelper.timeout(delay) метод, обслуживающий оба Hello и Random классы.

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

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

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

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

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

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

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

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

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

  • Имеет ли охват тестом быть 100%?

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

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

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

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

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

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

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

Что дальше?

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *