The post has been translated automatically. Original language: Russian
The previous part is "What is TDD and why is it needed". The next part is: "A good unit test: structure, naming, isolation."
In the first part, we found out that TDD is based on a short, repetitive cycle. In words, it looks simple, but in practice, beginners often make mistakes precisely in the step size: they write too much test or too much code at once. Let's analyze each phase and go through the cycle using a minimal example.
Three phases of the cycle
Red (red). One small test is being written for a behavior that does not yet exist. The test is running and should crash. It's important to see the fall: this is how you make sure that the test is able to verify something at all.
Green (green). The simplest code is written, which passes the test. Beauty and generality are not needed here, the task is the same: to get a green result.
Refactor (refactoring). When all the tests are green, the code and tests are put in order: duplicates are removed, names are improved. The behavior does not change, and the tests insure against accidental breakdowns.
Example: discount price calculation
Red. We start with a test for a function that doesn't exist yet.:
# test_discount.py
from discount import apply_discount
def test_ten_percent_discount():
assert apply_discount(price=200, percent=10) == 180 The startup gives an import error, and this is a normal red result.
Green. We are writing the simplest implementation.:
# discount.py
def apply_discount(price, percent):
return 180
It looks ridiculous, but the test passes. This technique is called "fake it": first we achieve green, then we generalize. Generalization imposes a second test on us:
def test_half_price():
assert apply_discount(price=100, percent=50) == 50
Now the constant doesn't fit, and the real formula appears.:
def apply_discount(price, percent):
return price * (100 - percent) / 100
A technique in which a general code is derived from several specific examples is called triangulation.
A new cycle and refactoring. Let's add a border check. First, as always, the test:
import pytest
def test_percent_out_of_range():
with pytest.raises(ValueError):
apply_discount(price=100, percent=120)
After the red result, add the if not 0 <= percent <= 100 check: raise ValueError(...). All three tests are green again. Now you can refactor, for example , to put the range check in a separate function or specify the names of the parameters. The behavior remains the same, and the green tests will immediately show if something is broken.
Rules of rhythm
- The step should take minutes, not hours.
- Tests are run after each change.
- You can only refactor on green.
- Do not add anything to the code that no test requires.
Result
The Red-Green-Refactor cycle is based on three things: a falling test, minimal code, and cleaning up during green tests. The small step size makes mistakes cheap. In the next part, we'll look at what the test itself should look like in order for it to be readable and reliable: structure, naming, and isolation.
What step size is comfortable for you in TDD, and have you ever had to "roll back" after taking too big a step? Tell us in the comments.
Предыдущая часть: «Что такое TDD и зачем он нужен». Следующая часть: «Хороший юнит-тест: структура, именование, изоляция».
В первой части мы выяснили, что TDD строится на коротком повторяющемся цикле. На словах он выглядит просто, но на практике новички чаще всего ошибаются именно в размере шага: пишут сразу слишком большой тест или слишком много кода. Разберём каждую фазу и пройдём цикл на минимальном примере.
Три фазы цикла
Red (красный). Пишется один небольшой тест для ещё не существующего поведения. Тест запускается и должен упасть. Важно увидеть падение: так вы убеждаетесь, что тест вообще способен что-то проверить.
Green (зелёный). Пишется самый простой код, который проходит тест. Красота и обобщённость здесь не нужны, задача одна: получить зелёный результат.
Refactor (рефакторинг). Когда все тесты зелёные, код и тесты приводятся в порядок: убираются дубли, улучшаются имена. Поведение при этом не меняется, а тесты страхуют от случайных поломок.
Пример: расчёт цены со скидкой
Красный. Начинаем с теста для функции, которой ещё нет:
# test_discount.py
from discount import apply_discount
def test_ten_percent_discount():
assert apply_discount(price=200, percent=10) == 180 Запуск даёт ошибку импорта, и это нормальный красный результат.
Зелёный. Пишем самую простую реализацию:
# discount.py
def apply_discount(price, percent):
return 180
Выглядит нелепо, но тест проходит. Этот приём называется «fake it» (сымитируй): сначала добиваемся зелёного, потом обобщаем. Обобщение нам навязывает второй тест:
def test_half_price():
assert apply_discount(price=100, percent=50) == 50
Теперь константа не подходит, и появляется настоящая формула:
def apply_discount(price, percent):
return price * (100 - percent) / 100
Приём, при котором общий код выводится из нескольких конкретных примеров, называется триангуляцией.
Новый цикл и рефакторинг. Добавим проверку границ. Сначала, как всегда, тест:
import pytest
def test_percent_out_of_range():
with pytest.raises(ValueError):
apply_discount(price=100, percent=120)
После красного результата добавляем проверку if not 0 <= percent <= 100: raise ValueError(...). Все три теста снова зелёные. Теперь можно провести рефакторинг, например вынести проверку диапазона в отдельную функцию или уточнить имена параметров. Поведение остаётся прежним, а зелёные тесты сразу покажут, если что-то сломалось.
Правила ритма
- Шаг должен занимать минуты, а не часы.
- Тесты запускаются после каждого изменения.
- Рефакторить можно только на зелёном.
- Не добавляйте в код то, что не требует ни один тест.
Итог
Цикл Red-Green-Refactor держится на трёх вещах: падающий тест, минимальный код и уборка при зелёных тестах. Малый размер шага делает ошибки дешёвыми. В следующей части разберём, как должен выглядеть сам тест, чтобы он был читаемым и надёжным: структура, именование и изоляция.
Какой размер шага комфортен вам в TDD, и приходилось ли вам «откатываться» после слишком большого шага? Расскажите в комментариях.