Разработка через тестирование существует дольше, чем вы, вероятно, думаете, примерно в 2003 году. Не так уж и давно, говорите? Что ж, на самом деле TDD — это всего лишь один из компонентов практики экстремального программирования, которая была впервые опубликована еще в 1999 году. В наши дни TDD — это то, чем ее называют большинство людей, поскольку она используется в сочетании с множеством других практик для построения рабочего процесса, который лучше всего подходит для конкретной задачи. команда. По крайней мере, в теории.

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

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

TDD — это повторение трех шагов:

  1. Тест не пройден. Напишите тест, который не пройден.
  2. Прохождение теста. Внесите изменения в код, чтобы пройти тест.
  3. Рефакторинг. Выполнение любой очистки или рефакторинга.

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

Ладно, хватит болтать, покажи!

Физз-ажиотаж

Некоторые из вас, возможно, слышали о Fizz Buzz. Это очень простой алгоритм, который хорошо подходит для первого знакомства с TDD. Он работает путем преобразования входного числа по следующим правилам:

  1. Если число делится на 3, мы должны вернуть «шипение».
  2. Если число делится на 5, мы должны вернуть «Buzz».
  3. Если число делится на 15, мы должны вернуть «FizzBuzz».
  4. Если ни одно из приведенных выше правил не соответствует, мы можем вернуть сам номер.

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

Я буду писать примеры кода на Python, но вы должны быть в состоянии следовать на любом вашем любимом языке.

1. Напишите тест, который не работает

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

import unittest
class TestFizzBuzz(unittest.TestCase):
  def test_3_is_fizz(self):
      self.assertEqual(fizz_buzz(3), 'Fizz')
# This will run the unit tests
unittest.main()

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

$ python fizzbuzz.py 
E
----------------------------------------------------------------------
Ran 1 test in 0.000s
FAILED (errors=1)

В python каждый запуск модульного теста представлен одним символом: .is — успех, E — ошибка (невозможно скомпилировать) и F — сбой (результат оказался не таким, как мы ожидали).

2. Внесите изменения, чтобы тест прошел

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

Самое простое решение — вернуть «Fizz»:

def fizz_buzz(number):
    return 'Fizz'

Повторный запуск тестов теперь успешен.

python fizzbuzz.py 
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

3. Рефакторинг

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

1. Напишите тест, который не работает

def test_5_is_buzz(self):
      self.assertEqual(fizz_buzz(5), 'Buzz')

Прежде чем продолжить, подтвердите ошибку.

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 5:
        return 'Buzz'
    return 'Fizz'

Обратите внимание, что мы использовали == 5 вместо % 5 == 0. Это позволит использовать для создания другого теста позже. Цель состоит в том, чтобы сгенерировать как можно больше неудачных тестов.

3. Рефакторинг

Опять же, здесь нечего рефакторить.

def test_15_is_fizzbuzz(self):
        self.assertEqual(fizz_buzz(15), 'FizzBuzz')

1. Напишите тест, который не работает

def test_15_is_fizzbuzz(self):
        self.assertEqual(fizz_buzz(15), 'FizzBuzz')

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number == 5:
        return 'Buzz'
    return 'Fizz'

3. Рефакторинг

Все равно пока ничего не нужно.

1. Напишите тест, который не работает

def test== 5is_1(self):
        self.assertEqual(fizz_buzz(1), 1)

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number == 5:
        return 'Buzz'
    if number == 1:
        return 1
    return 'Fizz'

3. Рефакторинг

Еще ничего.

1. Напишите тест, который не работает

def test% 5 == 0is_2(self):
        self.assertEqual(fizz_buzz(2), 2)

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number == 5:
        return 'Buzz'
    if number == 1 or number == 2:
        return number
    return 'Fizz'

3. Рефакторинг

Пока это первый рефакторинг. Мы понимаем, что эти операторы if теперь можно упростить:

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number == 5:
        return 'Buzz'
    if number == 3:
        return 'Fizz'
    return number

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

1. Напишите тест, который не работает

def test_6_is_fizz(self):
        self.assertEqual(fizz_buzz(6), 'Fizz’)

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number == 5:
        return 'Buzz'
    if number % 3 == 0:
        return 'Fizz'
    return number

3. Рефакторинг

На этот раз ничего.

1. Напишите тест, который не работает

def test_10_is_buzz(self):
        self.assertEqual(fizz_buzz(10), 'Buzz')

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number == 15:
        return 'FizzBuzz'
    if number % 5 == 0:
        return 'Buzz'
    if number % 3 == 0:
        return 'Fizz'
    return number

3. Рефакторинг

Еще ничего.

1. Напишите тест, который не работает

def test_30_is_fizzbuzz(self):
        self.assertEqual(fizz_buzz(30), 'FizzBuzz’)

2. Внесите изменения, чтобы тест прошел

def fizz_buzz(number):
    if number % 15 == 0:
        return 'FizzBuzz'
    if number % 5 == 0:
        return 'Buzz'
    if number % 3 == 0:
        return 'Fizz'
    return number

3. Рефакторинг

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

1. Напишите тест, который не работает

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

import unittest
def fizz_buzz(number):
    if number % 15 == 0:
        return 'FizzBuzz'
    if number % 5 == 0:
        return 'Buzz'
    if number % 3 == 0:
        return 'Fizz'
    return number
class TestFizzBuzz(unittest.TestCase):
    def test_3_is_fizz(self):
        self.assertEqual(fizz_buzz(3), 'Fizz')
def test_5_is_buzz(self):
        self.assertEqual(fizz_buzz(5), 'Buzz')
def test_15_is_fizzbuzz(self):
        self.assertEqual(fizz_buzz(15), 'FizzBuzz')
def test== 5is_1(self):
        self.assertEqual(fizz_buzz(1), 1)
def test% 5 == 0is_2(self):
        self.assertEqual(fizz_buzz(2), 2)
def test_6_is_fizz(self):
        self.assertEqual(fizz_buzz(6), 'Fizz')
def test_10_is_buzz(self):
        self.assertEqual(fizz_buzz(10), 'Buzz')
def test_30_is_fizzbuzz(self):
        self.assertEqual(fizz_buzz(30), 'FizzBuzz')
# This will run the unit tests
unittest.main()

Просто дайте мне написать ответ!

Когда вы следовали за ним, вы, вероятно, были в бешенстве от того факта, что потребовалось так много шагов (8 раундов), чтобы решить ответ, который вы, возможно, уже знали. Это справедливое беспокойство и, безусловно, самая большая первоначальная критика. Однако помните, что это должно было быть легко решено, что, если бы проблема была намного сложнее и не могла быть просто решена в вашей голове? Есть ли какой-либо другой процесс, который вы могли бы следовать, который почти гарантировал бы, что результат будет охватывать все сценарии?

Что можно сказать о решении…

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

Когда ваши требования постоянно меняются, это становится неоценимым.

Итак, что дальше?

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

Первоначально опубликовано на http://elliot.land 30 января 2016 г.