Pull to refresh

TDD — «утка». Введение в Quack-Driven Development. Современная waterfowl-методология для программистов

Reading time 10 min
Views 21K
Original author: Jason Hutchens


Предлагаю к ознакомлению вольный перевод статьи Джейсона Хатчерса «TDD(1) is ‘canard. Tell it to the Duck» или дословно: «TDD — Утка. Скажи это утке». (2)

Автор критикует «ортодоксальный» TDD подход к разработке и предлагает альтернативный.

И, да, в заголовке статьи нет опечатки (3).




Отчасти, цель жизни — веселье. Обычно, возможность сконцентрироваться на творческой стороне программирования доставляет программистам радость. Поэтому, Ruby создан, чтобы сделать программистов счастливыми.
Юкихиро Мацумото (также известный как «Matz»), 2000.


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


Умер ли TDD?


Я следил за дискуссией на тему "Is TDD dead?" с большим интересом, поскольку долго боролся с идеями, которые были резюмированы в виде "3-х основных правил TDD от дядюшки Боба":

  1. Запрещено писать какой-либо код кроме того, что нужен для прохождения проваленных тестов
  2. Запрещено писать дополнительные тесты, если текущего набора хватает для провала (проверка на компилируемость также является тестом)
  3. Запрещено писать дополнительный код, кроме кода, достаточного для прохода текущего проваленного теста.


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

Тренировка: игра в боулинг

Дядюшка Боб демонстрирует три правила TDD, показывая как реализовать механизм подсчёта очков в игре в боулинг. Давайте я вам продемонстрирую подход Дяюдшки Боба, четко следуя трем правилам TTD: (4):

  • Проверить что вы можете создать экземпляр игры в боулинг (провалено).
  • Реализовать пустой класс игры в боулинг (пройдено).
  • Проверить что созданный экземпляр возвращает 0 очков, при постоянном попадании шара в желоб (провалено).
  • Реализовать функцию подсчёта очков, которая всегда возвращает 0 (пройдено).
  • Проверить что количество очков равно 20 если на каждом ходе сбивается 1 кегля (провалено).
  • Изменить функцию подсчёта очков: возвращать сумму сбитых кеглей (пройдено).
  • Проверить что счёт корректно вычисляется при броске спэра (дополнительного броска) (провалено).
  • … (и так далее)


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

Зачем же тестировать?


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

  1. Документация. Для обеспечения понимания другими программистами. (Документация устаревает. Тесты не устаревают если вы следите, чтобы они проходили. Также, они описывают код в виде примеров его использования, которые являются лучшей документацией для программиста.)
  2. Дизайн. Зафиксировать требования к продукту и использовать их в качестве границ для разработки.
  3. Защищённость. Сделать так, чтобы другим программистам было проще делать всё правильно.
  4. Отладка. Воспроизводить ошибки до исправления их первопричин. (Всегда лучше написать тест, который воспроизводит ошибку, чем производить традиционную отладку, поскольку это гарантирует, что данный вид ошибки не появится вновь.)
  5. Соглашение. Договориться о форматах ввода/вывода функций.
  6. Безопасность. Увеличить доверие к продукту при использовании динамических, бестиповых языков.
  7. Мотивация. Пометить небольшой фронт работ, которым вы собираетесь заниматься дальше.(Когда не знаешь как подступиться новому проекту, с чего начать, красный значок напротив теста подскажет, что можно сделать здесь и сейчас (правильно — сделать так, чтобы рядом с тестом стоял зелёный значок)).
  8. Фокус. Предотвратить возникновение раздутой или излишне абстрактной реализации.
  9. Путеводитель. Определить финишную черту, чтобы вы знать, когда работу можно считать завершённой.
  10. Эффективность. Использовать время, выделенное на ручное тестирование, с толком. (Зачастую сборка и запуск приложения на несколько порядков сложнее, чем запуск тестов. Вы же не хотите ждать 20 минут, пока идёт компиляция вашей игры для консоли и её перенос в SDK, чтобы увидеть, как она тут же падает на старте. Тест выявил бы ошибку значительно быстрее.)


Большинство из описанных выше причин писать тесты не связаны, явным образом, с тремя правилами TDD и фазой рефакторинга из цикла «красный-зеленый-рефакторинг» (5).

Забавно, что некоторые из причин тестирования, которые я даю выше, являются также вескими причинами для написания технической документации. Фактически…

Тесты в качестве документации

… часто говорят, что тесты являются наилучшей формой технической документации.

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

Если тесты лучшая форма документации, и, если, документация должна быть внедрена в код, то… может, и тесты также следует писать рядом с кодом? Конечно следует, если разделить их на две части: валидацию и примеры.

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


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

Введение в Quack-Driven Development


Quack-Driven Development, или QDD, позволяет программисту выразить требования, соглашения и намерения внутри самой реализации.

Это современная методология «водоплавающих» (waterfowl) для разработки программного обеспечения.

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

  • Понимайте задачу.
    Не путайте задачу и её решение. Всегда будьте уверены, что вы делаете по настоящему важные вещи. Прочтите «Are Your Lights On» для вдохновения.
  • Доверяйте своим предположениям.
    Лучше делать проверку корректности данных в интерфейсе. Не загромождайте бизнес-логику из-за того, что вы не доверяете данным.
  • Уважайте читателя.
    Пишите код, который смогут прочесть другие программисты и вы сами (через год). Пишите код, который вам нравится читать. Показывайте, а не рассказывайте.

Следующая далее реализация QDD в Ruby, является доказательством правильности концепции QDD. Она позволяет программистам «поговорить с уткой» в коде, используя «утиный смайлик» (Q<), и демонстрирует, как я бы использовать QDD подход, чтобы реализовать пример игры в боулинг дядюшки Боба. Приступим.

Заложим основу приложения

class Game
  def initialize
    @frames = []
  end
  Q< "принимает количество сбитых кегель"
  def roll(pins)
    Q< "вызывается каждый раз, когда игрок бросает шар"
  end
  Q< "возвращает итоговый счёт игры"
  def score
    Q< "вызывается только в конце игры"
  end
end

  • Хорошенько подумаем, прежде чем делать набросок реализации.
  • Четыре «утки» внедрены в код. Они задают четыре требования явным и проверяемым образом.


Зададим «уток»

class Game
  Q< :roll do
    before "принимает количество сбитых кегель" do
      expect(pins).to be_a(FixNum)
      expect(1..10).to cover(pins)
    end
 
    within "вызывается каждый раз, когда игрок бросает шар" do
      expect(@frames).to be_a(Array)
      expect(1..10).to cover(@frames.length)
    end
  end
  Q< :score do
    within "вызывается только в конце игры" do
      expect(@frames.length).to eq(10)
    end
    after "возвращает итоговый счёт игры" do
      expect(retval).to be_a(FixNum)
      expect(0..300).to cover(retval)
    end
  end
end

  • «Утки» написаны в том же классе, но могут быть сохранены и в отдельном файле.
  • Они могут быть определены для запуска перед вызовом метода, внутри вызова метода или после того, как метод был выполнен.
  • Они выполняются в области видимости метода, который проходит тестирование, поэтому локальные переменные, определенные в теле метода, доступны внутри «уток».
  • Они используют rspec-ожидания для проверки состояния.

Добавим реализацию

class Game
  def initialize
    @frames = []
  end
  Q< "принимает количество сбитых кегель"
  def roll(pins)
    Q< "вызывается каждый раз, когда игрок бросает шар"
 
    @frames << Frame.new if _start_new_frame?
    Q< "фрейм, которому необходим бросок должен быть тут"
    Q< "если мы на 10-м фрейме,фрйем может быть завершенным"
    @frames.each do |frame|
      frame.bonus(pins) if frame.needs_bonus_roll?
    end
 
    @frames.last.roll(pins) if @frames.last.needs_roll?
  end
  Q< "возвращает итоговый счёт игры"
  def score
    Q< "вызывается только в конце игры"
    @frames.map(&:score).reduce(:+)
  end
  private
  def _start_new_frame?
    @frames.size == 0 ||
      @frames.size < 10 && !@frames.last.needs_roll?
  end
end

class Frame
  def initialize
    @rolls = [] ; @bonus_rolls = []
  end
  Q< "возвращает, ждет ли этот фрейм броска"
  def needs_roll?
    !_strike? && @rolls.size < 2
  end
  Q< "принимает количество сбитых кегель"
  def roll(pins)
    Q< "вызывается только когда фрейм ждет броска"
    @rolls << pins
  end
  Q< "возврвщает ждёт ли фрейм бонусного броска"
  def needs_bonus?
    _strike? && @bonus_rolls.size < 2 ||
      _spare? && @bonus_rolls.size < 1
  end
  Q< "принимает количество сбитых кегель"
  def bonus_roll(pins)
    Q< "вызывается только когда фрейм ждет бонусного броска"
    @bonus_roll << pins
  end
  Q< "возвращает итоговый счёт по фрейму"
  def score
    Q< "вызывается только когда больше нет бросков"
    @rolls.reduce(:+) + @bonus_rolls.reduce(0, :+)
  end
  private
  def _strike?
    @rolls.first == 10
  end
  def _spare?
    @rolls.reduce(:+) == 10
  end
end

  • Обратите внимание, что, параллельно с уточнением и развитием реализации, было добавлено больше «уток». Я не показал их определения для краткости.
  • Код получился таким из эстетических побуждений. Например, я бы особо отметил, что Game#score рассчитывается путем суммирования баллов, набранных в каждом фрейме. Я сделал так не потому, что так код выглядит более универсальным и повторно используемым, и не потому, что так его проще проверять, а потому, что я буду объяснять суть этого кода коллеге именно так, как он (код) уже написан.
  • Размышляя о реализации в том же ключе, можно обнаружить, что механизм подсчёт очков в реальной жизни содержит уродливый хак: игрок может бросать шар до трех раз в десятом фрейме. Не стоит реализовывать этот хак в коде, так как, очевидно, что каждый фрейм стостоит из двух различных видов бросков. Броски текущего фрейма и бонусные броски, которые будут реализованы в будущем, когда текущий фрейм будет завершен. Такое разделение также было продиктовано эстетикой и служит для более четкого понимания реализации.

Напишем несколько примеров

describe Game do
  let(:game) { Game.new }
  it “возвратит 0 если шар ушёл в желоб” do
    20.times { game.roll(0) }
    expect(game.score).to eq(0)
  end
 
  it "возвратит 20 если все кегли забиты" do
    20.times { game.roll(1) }
    expect(game.score).to eq(20)
  end
  it "учитывает спэры" do
    game.roll(5)
    game.roll(5)
    game.roll(3)
    17.times { game.roll(0) }
    expect(game.score).to eq(16)
  end
  it "учитывает броски" do
    game.roll(10)
    game.roll(3)
    game.roll(4)
    16.times { game.roll(0) }
    expect(game.score).to eq(24)
  end
  it "возвратит 300 в случае идеальной игры" do
    12.times { game.roll(10) }
    expect(game.score).to eq(300)
  end
  it “должна работать с игрой, заданной в примере” do
    [1,4,4,5,6,4,5,5,10,0,1,7,3,6,4,10,2,8,6].each do |pins|
      game.roll(pins)
    end
    expect(game.score).to eq(133)
  end
  it “не должна работать, если игра некорректна” do
    expect(game.score).to quack("вызывается только в конце игры")
    expect(game.roll(11)).to quack("принимает количество сбитых кегель")
    expect(game.roll(5.5)).to quack("принимает количество сбитых кегель")
    expect(game.roll(nil)).to quack
    expect(30.times { game.roll(0) }).to quack
  end
end


  • Это всего лишь стандартные rspec тесты, но они также проверяют «уток».
  • Они могут быть записаны до, после или вперемешку с реализацией.
  • Первые пять тестов являются зеркальным отражением тестов из тренировки дядюшки Боба. Их задача — управлять реализацией, в не проверять правильность исходного кода.
  • Мы также проверили пример, который был дан в требованиях к тренировке — некорректное использование тоже обрабатывается.


«Утки» в рабочей среде

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

А почему бы и нет! Поведение «уток» может быть настроено произвольным образом:

  • Выбросить исключение: Выбросить исключение. Поведение по умолчанию.
  • Зажурналировать: Зажурналировать ошибку и продолжить.
  • Отладить: Перейти в отладчик.
  • Игнорировать: Ничего не делать вообще.

«Уток» также легко можно закомментировать, превращая их в мертвых уток, как в следующем примере:

class Game
  #< "принимает количество сбитых кегель"
  def roll(pins)
    #< "вызывается каждый раз, когда игрок бросает шар"
  end
  #< "возвращает итоговый счёт игры"
  def score
    #< "вызывается только в конце игры"
  end
end

Обратите внимание, что даже «мертвые утки» служат комментариями. Являются ли комментарии, которые вы пишете в коде сегодня, всего лишь «мёртвыми утками», которые ждут своего часа, чтобы «вернуться к жизни» посредством QDD?

Итоги


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

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

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

Три правила QDD
  1. Создайте скелет реализации с «утками», которые фиксируют требования, интерфейсные соглашения и намерения программиста.
  2. Пишите тесты, которые проверяют «уток» и охватывают макет и пишите реализацию, чтобы эти тесты проходили. Не важно в каком порядке.
  3. Запустите «уток» «жить» в рабочую среду, предоставляя своим клиентам реальные примеры, а не ваши мудрёные спецификации!

Преимущества QDD

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

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

Я рекомендую вам сегодня же добавить QDD-подход к вашей методологии «водоплавающих», так как он прекрасно дополняет утиную типизацию, избиение утки(6) и метод утёнка!

Q< «Тестирование никогда не превратится в „утку“!»



  1. TDD — Test Driven Development — техника разработки ПО через тестирование.
  2. Игра слов: «утка» — ложный слух, в первом случае, резиновая уточка для разговоров — во втором.
  3. Очередная игра слов: Каскадная модель разработки ПО (waterfall) превращается в «методологию водоплавающих птиц» (waterfowl).
  4. Чтобы легче понять о чём идет речь, ознакомьтесь с презентацией BowlingGameKata.ppt
  5. Подробнее в разделе «Стиль разработки» ru.wikipedia.org/wiki/Разработка_через_тестирование
  6. У нас используется выражение обезьяний патч.

Tags:
Hubs:
+14
Comments 21
Comments Comments 21

Articles