Разработка через тестирование (TDD) инвертирует обычный порядок написания кода: сначала пишется тест до какой-либо реализации, затем пишется ровно столько кода, чтобы тест прошёл, затем код улучшается. Дисциплина выражена в трёхфазном цикле под названием Красный-Зелёный-Рефакторинг.
## Цикл Красный-Зелёный-Рефакторинг
**Красный** -- напишите тест для поведения, которого ещё не существует. Запустите его. Он падает -- вывод красный. Это намеренно и правильно. Падающий тест до любой реализации доказывает, что тест действительно проверяет что-то реальное. Если совершенно новый тест проходит без каких-либо изменений кода, либо поведение уже реализовано в другом месте, либо тест неверен.
**Зелёный** -- напишите минимальный код, который делает падающий тест проходящим. Ничего лишнего. Никакого кода "на будущее". Никаких излишеств. Единственная цель -- зелёный вывод. Напишите простейшее, что может работать.
**Рефакторинг** -- теперь, когда код работает, улучшите его. Устраните дублирование, переименуйте для ясности, выделите вспомогательные функции, реорганизуйте. Тесты защищают вас: если рефакторинг что-то сломает, вы узнаете немедленно. Это фаза, в которой пишется хороший код -- не в фазе Зелёный.
```
[Красный] Написать падающий тест для следующего поведения
|
v
[Зелёный] Написать минимальный код для прохождения теста
|
v
[Рефакторинг] Улучшить код, пока тесты остаются зелёными
|
v
[Красный] Написать следующий падающий тест ...
```
Каждый цикл короткий -- несколько минут максимум. Цикл намеренно мал. Вы никогда не накапливаете большие объёмы непроверенного кода.
## Зачем писать тест первым?
Аргументы в пользу test-first -- не просто дисциплина, они меняют то, что вы строите и как вы это строите.
**Тесты, написанные после кода, описывают что код делает, а не что он должен делать.** Когда вы пишете тесты ретроспективно, вы уже знаете реализацию, поэтому неосознанно подтверждаете существующее, а не указываете задуманное. Граничные случаи, которые не реализованы, не тестируются, потому что вы о них не подумали.
**Трудно тестируемый код сигнализирует о проблеме дизайна.** Функция, требующая 8 шагов настройки перед вызовом, трудна для написания теста -- и трудна для использования в продакшне. С TDD вы обнаруживаете это до того, как код написан. Боль от сложного теста ведёт к лучшему дизайну.
**Тест -- первый клиент вашего API.** Написание теста -- это первый раз, когда вы "вызываете" свой код. Если вызов в тесте ощущается неудобным, API неудобен. TDD выявляет проблемы API, пока их ещё можно исправить с нулевыми затратами.
**Покрытие спецификации автоматическое.** Каждая строка реализации существует потому, что её потребовал падающий тест. Если ни один тест не потребовал строку, она не пишется.
## Когда TDD оправдывает себя
TDD приносит наибольшую пользу когда:
- **Логика нетривиальна.** Правила валидации, бизнес-ограничения, автоматы состояний, разбор, вычисления с множеством ветвящихся условий -- любой код, где корректность зависит от тонких комбинаций входных данных.
- **Вы проектируете API.** TDD -- инструмент проектирования. Тест, который легко написать, предполагает хороший API; болезненный тест предполагает плохой. Трение появляется до того, как код зафиксирован.
- **Вы будете рефакторить.** Рефакторинг без тестов -- это угадывание. С TDD каждое поведение имеет тест до начала любого рефакторинга -- вы можете свободно реструктурировать и доверять тестам обнаружение регрессий.
- **Корректность важнее скорости.** Финансовые вычисления, чувствительный к безопасности код, преобразования данных с граничными случаями -- места, где тихий неверный ответ хуже очевидного падения.
## Когда TDD менее ценен
TDD подходит не для каждой ситуации:
- **Исследовательские спайки.** Когда вы ещё не знаете, что строите -- прототипируете подход, изучаете незнакомую библиотеку, пробуете алгоритм -- сначала напишите спайк для обучения, затем выбросьте его и начните TDD с реальной реализацией. Тесты на одноразовый код -- это потери.
- **Тонкий клей-код.** Определения маршрутов, файлы миграций, декларативная конфигурация -- код без логики для тестирования. Тест на то, что Django сопоставил `/users/` с `UserView`, не добавляет уверенности.
- **UI-рендеринг и вёрстка.** Утверждения о пиксельных позициях или поведении анимаций хрупки и требуют много обслуживания. Тестируйте логику, питающую UI; используйте более лёгкие инструменты для UI-интеграции.
- **Тонкие обёртки внешних API.** `return requests.get(url).json()` не имеет ничего значимого для тестирования кроме "вызвали ли мы requests". Такие тесты хрупки и не добавляют уверенности.
## Как TDD формирует структуру кода
Код, выращенный test-first, склонен приобретать определённые структурные свойства -- не из-за дисциплины, а потому что тесты требуют этого во время разработки:
**Меньшие функции.** Функция, требующая 8 аргументов или глубокой настройки для вызова, болезненна для тестирования. Боль появляется до того, как код существует, поэтому вы делаете функцию меньше.
**Явные зависимости.** Если функции нужно соединение с базой данных, тест должен его предоставить. Путь наименьшего сопротивления -- сделать зависимость параметром (внедрение зависимостей), а не скрытым импортом на уровне модуля. TDD немедленно делает скрытые глобалы и импорты с побочными эффектами болезненными.
**Больше чистых функций.** Функции, принимающие аргументы и возвращающие значения -- без общего состояния, без файлового ввода-вывода, без сети -- проще всего тестировать. TDD последовательно подталкивает вас писать их больше.
**Минимальные, связные классы.** Класс, делающий слишком много, требует слишком много настройки в каждом тесте. Трение теста сигнализирует о проблеме дизайна до того, как она закрепляется.
## Распространённые возражения
**"TDD занимает вдвое больше времени."**
Тест нужно написать в какой-то момент -- вопрос в том, когда. Написание первым направляет реализацию и часто делает её быстрее. Настоящий пожиратель времени -- отладка непроверенного кода постфактум. TDD переносит затраты с отладки на проектирование, что является лучшим компромиссом.
**"Я ещё не знаю, что строю."**
Не нужно знать весь дизайн заранее. Нужно знать только следующее поведение. Напишите один тест для наименьшей следующей вещи. TDD итеративен -- вы обнаруживаете дизайн через тесты, а не до них.
**"Мой код невозможно тестировать."**
Нетестируемый код -- симптом проблем дизайна: тесная связанность, скрытые зависимости, глобальное состояние. TDD, применённый к новому коду, предотвращает их возникновение. Применённый к существующему коду, TDD действует как предохранительный клапан -- он делает проблемы дизайна видимыми как боль при тестировании, мотивируя вас их устранять.
## Приложение, которое мы построим
Класс `BankAccount` с методами `deposit`, `withdraw` и `balance`, выращенный с помощью строгого TDD -- по одному тесту за раз, минимум кода для прохождения, затем рефакторинг. Два файла:
```
bank.py # продакшн-код -- начинается пустым
test_bank.py # тесты -- растёт по одному тесту за раз
```
В начале оба файла пусты.
---
## Цикл 1: у нового счёта нулевой баланс
**Красный** -- напишем первый тест:
```python
# test_bank.py
from bank import BankAccount
def test_new_account_has_zero_balance():
account = BankAccount()
assert account.balance == 0
```
Запускаем:
```
$ pytest test_bank.py -v
FAILED - ModuleNotFoundError: No module named 'bank'
```
Хорошо -- падает. У нас есть разрешение писать код.
**Зелёный** -- абсолютный минимум:
```python
# bank.py
class BankAccount:
def __init__(self):
self.balance = 0
```
```
1 passed
```
**Рефакторинг** -- пока нечего чистить.
---
## Цикл 2: депозит увеличивает баланс
**Красный:**
```python
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(100)
assert account.balance == 100
```
```
FAILED - AttributeError: 'BankAccount' object has no attribute 'deposit'
```
**Зелёный:**
```python
def deposit(self, amount):
self.balance += amount
```
```
2 passed
```
**Рефакторинг** -- всё ещё нечего дедублировать.
---
## Цикл 3: снятие уменьшает баланс
**Красный:**
```python
def test_withdraw_decreases_balance():
account = BankAccount()
account.deposit(200)
account.withdraw(50)
assert account.balance == 150
```
**Зелёный:**
```python
def withdraw(self, amount):
self.balance -= amount
```
```
3 passed
```
---
## Цикл 4: овердрафт вызывает ValueError
Здесь тест вынуждает нас указать контракт -- тип ошибки, сообщение -- до того, как существует какой-либо код:
**Красный:**
```python
import pytest
def test_withdraw_raises_when_insufficient_funds():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Insufficient funds"):
account.withdraw(150)
```
```
FAILED - DID NOT RAISE <class 'ValueError'>
```
**Зелёный:**
```python
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
4 passed
```
Сообщение об ошибке `"Insufficient funds"` было решено в тесте -- спецификация управляет деталью реализации.
---
## Цикл 5: сумма депозита должна быть положительной
**Красный:**
```python
def test_deposit_rejects_non_positive_amount():
account = BankAccount()
with pytest.raises(ValueError, match="Amount must be positive"):
account.deposit(-50)
```
**Зелёный:**
```python
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
```
```
5 passed
```
---
## Цикл 6: сумма снятия тоже должна быть положительной
Снятие нуля или отрицательного числа молча испортит баланс. Тест поймает это до того, как это может случиться:
**Красный:**
```python
def test_withdraw_rejects_non_positive_amount():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Amount must be positive"):
account.withdraw(0)
```
**Зелёный:**
```python
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
6 passed
```
---
## Рефакторинг: выделить `_validate_positive`
Оба `deposit` и `withdraw` теперь имеют идентичную валидацию. До того, как это разрослось, выделим её:
**Шаг 1** -- добавить вспомогательную функцию (все 6 тестов по-прежнему проходят):
```python
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
```
**Шаг 2** -- заменить дублирующиеся проверки (все 6 тестов по-прежнему проходят):
```python
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
def withdraw(self, amount):
self._validate_positive(amount)
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
6 passed
```
Рефакторинг безопасен, потому что каждое поведение имеет тест.
---
## Итоговое состояние обоих файлов
```python
# test_bank.py
import pytest
from bank import BankAccount
def test_new_account_has_zero_balance():
account = BankAccount()
assert account.balance == 0
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(100)
assert account.balance == 100
def test_withdraw_decreases_balance():
account = BankAccount()
account.deposit(200)
account.withdraw(50)
assert account.balance == 150
def test_withdraw_raises_when_insufficient_funds():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Insufficient funds"):
account.withdraw(150)
def test_deposit_rejects_non_positive_amount():
account = BankAccount()
with pytest.raises(ValueError, match="Amount must be positive"):
account.deposit(-50)
def test_withdraw_rejects_non_positive_amount():
account = BankAccount()
account.deposit(100)
with pytest.raises(ValueError, match="Amount must be positive"):
account.withdraw(0)
```
```python
# bank.py
class BankAccount:
def __init__(self):
self.balance = 0
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
def withdraw(self, amount):
self._validate_positive(amount)
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
```
---
## Что дал нам TDD
- 6 поведений, каждое указанное тестом до того, как существовал код.
- Сообщения об ошибках были решены в тестах -- спецификация управляет деталями реализации, а не наоборот.
- Вспомогательная функция `_validate_positive` появилась из рефакторинга, а не из предварительного проектирования. Тест-сьют сделал её безопасной для выделения.
- В `bank.py` нет кода, не покрытого тестом -- каждая строка там потому, что её потребовал падающий тест.
- В любой точке цикла запуск `pytest -v` говорит вам точно, где вы находитесь.
## Цикл TDD
```
1. [Красный] Написать падающий тест для следующего поведения
2. [Зелёный] Написать минимальный код для его прохождения -- ничего лишнего
3. [Рефакторинг] Навести порядок в коде, пока все тесты зелёные
4. Повторить для следующего поведения
```
## Команды pytest для TDD
```bash
# Остановиться на первом падении -- самая быстрая обратная связь при реализации
pytest -x
# Запустить только тесты, соответствующие шаблону имени
pytest -k "withdraw"
# Запустить один конкретный тест
pytest test_bank.py::test_withdraw_raises_when_insufficient_funds -v
# Подробный вывод: видеть имя каждого теста и результат
pytest -v
# Режим наблюдения: перезапускать при сохранении файла (требует pytest-watch)
pip install pytest-watch
ptw -- -x
```
## Именование тестов для TDD
Хорошие имена читаются как спецификация. Паттерн: `test_<субъект>_<поведение>_<условие>`.
| Имя | Читается как |
|---|---|
| `test_new_account_has_zero_balance` | "У нового счёта нулевой баланс" |
| `test_withdraw_raises_when_insufficient_funds` | "Снятие вызывает ошибку при недостаточных средствах" |
| `test_deposit_rejects_non_positive_amount` | "Депозит отвергает неположительные суммы" |
| `test_email_validator_returns_false_for_empty_string` | Условие явное |
Избегайте: `test_bank_1`, `test_edge_case`, `test_stuff`. Когда тест падает, его имя должно говорить что сломалось без чтения тела.
## Проверка фазы Красный
Новый тест, немедленно проходящий (без изменений кода), означает одно из двух:
1. Поведение уже реализовано -- оставьте тест как защиту от регрессий.
2. Тест неверен -- он не упражняет поведение, которое вы думаете.
Для проверки: временно сломайте реализацию и убедитесь что тест краснеет:
```python
def deposit(self, amount):
pass # намеренно сломано
# Если test_deposit_increases_balance теперь ПАДАЕТ -> тест верен
# Если он по-прежнему ПРОХОДИТ -> тест не тестирует deposit
```
Восстановите реализацию после проверки.
## Безопасный рефакторинг
Правило: **все тесты должны проходить до и после каждого шага рефакторинга**. Никогда не рефакторьте, пока любой тест красный.
```python
# До: дублирование в двух методах
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
def withdraw(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
...
# Шаг рефакторинга 1: добавить вспомогательную функцию (тесты по-прежнему проходят)
def _validate_positive(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
# Шаг рефакторинга 2: заменить дубликаты (тесты по-прежнему проходят)
def deposit(self, amount):
self._validate_positive(amount)
self.balance += amount
```
Делайте одно маленькое изменение, запускайте тесты, убеждайтесь в зелёном, затем делайте следующее изменение.
## TDD с parametrize
`@pytest.mark.parametrize` органично вписывается в TDD: напишите полную спецификацию (все тест-кейсы) до любой реализации. Все кейсы падают одновременно (Красный); одна реализация делает их все проходящими (Зелёный):
```python
import pytest
@pytest.mark.parametrize("amount,is_valid", [
(100, True),
(0.01, True),
(0, False),
(-50, False),
])
def test_deposit_validates_amount(amount, is_valid):
account = BankAccount()
if is_valid:
account.deposit(amount)
assert account.balance == pytest.approx(amount)
else:
with pytest.raises(ValueError):
account.deposit(amount)
```
Вывод parametrize показывает каждый кейс отдельно -- вы можете отслеживать прогресс через фазу Красный, когда кейсы зеленеют один за другим.
## Характеризующие тесты для легаси-кода
При добавлении тестов к существующему коду без тестов пишите "характеризующие тесты" -- тесты, документирующие то, что код делает сейчас:
```python
# Шаг 1: написать тесты для фиксации текущего поведения (они проходят немедленно)
def test_word_count_splits_on_spaces():
stats = TextStats("hello world foo")
assert stats.word_count() == 3
# Шаг 2: рефакторить реализацию, держа тесты зелёными
# Шаг 3: добавить новые тесты для нового поведения
```
Тесты не указывают что код должен делать -- они фиксируют что он делает. После фиксации можно рефакторить безопасно.
## Делаем время-зависимый код тестируемым
Внедряйте время как зависимость вместо прямого вызова `time.time()`:
```python
import time
class RateLimiter:
def __init__(self, calls_per_second, time_fn=None):
self.limit = calls_per_second
self._time = time_fn if time_fn is not None else time.time # None = реальные часы
self._window_start = self._time()
self._call_count = 0
def allow(self):
now = self._time()
if now - self._window_start >= 1.0:
self._window_start = now
self._call_count = 0
if self._call_count < self.limit:
self._call_count += 1
return True
return False
```
В тестах передавайте фиктивные часы -- `time.sleep` не нужен:
```python
fake_time = [0.0]
limiter = RateLimiter(2, time_fn=lambda: fake_time[0])
fake_time[0] = 1.5 # мгновенно сдвинуть время
```
## Распространённые ошибки TDD
| Ошибка | Последствие | Исправление |
|---|---|---|
| Написать несколько тестов до первого Зелёного | Каскад падений, сложно изолировать | По одному тесту за раз |
| Фаза Зелёный добавляет больше кода, чем нужно | Неспецифицированное поведение без тестов | Сделать только падающий тест проходящим |
| Пропуск фазы Рефакторинг | Технический долг накапливается каждый цикл | Всегда рефакторить, пусть и кратко |
| Не подтверждать Красный перед написанием кода | Тест может ничего не проверять | Всегда смотреть как тест падает |
| Тестировать внутренности реализации | Тесты ломаются при рефакторинге, не при изменении поведения | Тестировать поведение (вход/выход), не внутренности |
| Рефакторинг крупными шагами | Сложно определить какой шаг сломал тест | Маленькие шаги, запускать тесты между каждым |
Используя TDD, реализуйте класс `Stack` с четырьмя методами: `push(item)`, `pop()`, `peek()` и `is_empty()`.
**Шаг 1 -- Красный:** Сначала заполните все 7 тел тестов. Запустите pytest -- все 7 должны упасть с `AttributeError` или `AssertionError`. Не трогайте `stack.py` пока.
Тесты для написания:
1. `test_new_stack_is_empty` -- `Stack().is_empty()` возвращает `True`
2. `test_push_makes_stack_non_empty` -- после `push(1)`, `is_empty()` возвращает `False`
3. `test_pop_returns_last_pushed_item` -- добавить 1, 2, 3; `pop()` возвращает `3`
4. `test_pop_removes_the_item` -- после push и pop, `is_empty()` возвращает `True`
5. `test_peek_returns_top_without_removing` -- `peek()` возвращает верхний элемент; `is_empty()` по-прежнему `False` после
6. `test_pop_raises_on_empty_stack` -- `pop()` на пустом стеке вызывает `IndexError`
7. `test_peek_raises_on_empty_stack` -- `peek()` на пустом стеке вызывает `IndexError`
**Шаг 2 -- Зелёный/Рефакторинг:** Реализуйте `Stack` по одному методу за раз, запуская pytest после каждого добавления.
Используя TDD, реализуйте `validate_email(s: str) -> bool`.
**Шаг 1 -- Красный:** Сначала напишите все 5 тел тестов. Запустите pytest -- все 5 падают потому что функция всегда возвращает `None` (что является ложным значением). Ничего не реализуйте пока.
Необходимые тесты:
1. `test_valid_email_returns_true` -- `"[email protected]"` -> `True`
2. `test_missing_at_sign_returns_false` -- `"userexample.com"` -> `False`
3. `test_missing_local_part_returns_false` -- `"@example.com"` -> `False`
4. `test_missing_domain_returns_false` -- `"user@"` -> `False`
5. `test_empty_string_returns_false` -- `""` -> `False`
**Шаг 2 -- Зелёный:** Реализуйте одно правило валидации за раз. После каждого правила запускайте pytest. Добавляйте только код, необходимый для прохождения текущего падающего теста.
# email_validator.py
def validate_email(s: str) -> bool:
if not s:
return False
if '@' not in s:
return False
local, _, domain = s.partition('@')
if not local:
return False
if not domain:
return False
return True
# test_email.py
from email_validator import validate_email
def test_valid_email_returns_true():
assert validate_email("[email protected]") is True
def test_missing_at_sign_returns_false():
assert validate_email("userexample.com") is False
def test_missing_local_part_returns_false():
assert validate_email("@example.com") is False
def test_missing_domain_returns_false():
assert validate_email("user@") is False
def test_empty_string_returns_false():
assert validate_email("") is False
03
Написать характеризующие тесты для легаси-кода, затем безопасно рефакторить
Вам дан класс `TextStats` с существующим поведением, но без тестов. Ваша задача состоит из двух частей:
**Часть 1 -- Характеризация:** Напишите 4 теста, документирующих текущее поведение. Эти тесты должны проходить немедленно после заполнения (код уже работает). Они не задают новое поведение -- они фиксируют существующее, чтобы рефакторинг был безопасным.
Тесты для написания:
1. `test_word_count` -- `TextStats("hello world foo").word_count()` возвращает `3`
2. `test_char_count` -- `TextStats("hello").char_count()` возвращает `5`
3. `test_most_common_word` -- `TextStats("cat dog cat").most_common_word()` возвращает `"cat"`
4. `test_most_common_word_is_case_insensitive` -- `TextStats("Cat cat Dog").most_common_word()` возвращает `"cat"`
**Часть 2 -- Рефакторинг:** Когда все 4 теста проходят, перепишите `most_common_word` с использованием `collections.Counter`. Запустите тесты после -- они должны по-прежнему проходить.
```python
# text_stats.py -- дано, не изменять в Части 1
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
best, best_count = None, 0
for word, count in counts.items():
if count > best_count:
best, best_count = word, count
return best
```
# text_stats.py -- дано, не изменять в Части 1
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
counts = {}
for word in words:
if word in counts:
counts[word] += 1
else:
counts[word] = 1
best, best_count = None, 0
for word, count in counts.items():
if count > best_count:
best, best_count = word, count
return best
# test_text_stats.py
from text_stats import TextStats
def test_word_count():
pass
def test_char_count():
pass
def test_most_common_word():
pass
def test_most_common_word_is_case_insensitive():
pass
Решение
# test_text_stats.py
from text_stats import TextStats
def test_word_count():
stats = TextStats("hello world foo")
assert stats.word_count() == 3
def test_char_count():
stats = TextStats("hello")
assert stats.char_count() == 5
def test_most_common_word():
stats = TextStats("cat dog cat")
assert stats.most_common_word() == "cat"
def test_most_common_word_is_case_insensitive():
stats = TextStats("Cat cat Dog")
assert stats.most_common_word() == "cat"
# text_stats.py -- после рефакторинга
from collections import Counter
class TextStats:
def __init__(self, text: str):
self.text = text
def word_count(self):
return len(self.text.split())
def char_count(self):
return len(self.text)
def most_common_word(self):
words = self.text.lower().split()
if not words:
return None
return Counter(words).most_common(1)[0][0]
04
TDD с время-зависимым RateLimiter -- внедряем часы
Используя TDD, реализуйте класс `RateLimiter(calls_per_second, time_fn=None)` с единственным методом `allow() -> bool`.
Параметр `time_fn` принимает любой вызываемый объект, возвращающий текущее время как float. По умолчанию `time.time`. Это делает часы внедряемыми -- тесты передают фиктивные часы вместо ожидания.
**Шаг 1 -- Красный:** Напишите все 5 тестов с фиктивными часами (без `time.sleep`). Все падают, потому что `RateLimiter` ещё не существует.
Тесты для написания:
1. `test_first_call_is_always_allowed`
2. `test_calls_within_limit_are_allowed` -- при limit=2 два вызова при t=0 оба `True`
3. `test_calls_exceeding_limit_are_denied` -- при limit=2 третий вызов при t=0 равен `False`
4. `test_calls_reset_after_one_second` -- после продвижения фиктивного времени до 1.0 вызовы снова разрешены
5. `test_two_limiters_are_independent` -- исчерпание одного ограничителя не влияет на другой
**Шаг 2 -- Зелёный/Рефакторинг:** Реализуйте `RateLimiter` используя фиксированное окно в 1 секунду. Окно сбрасывается когда `now - window_start >= 1.0`.
Используя TDD с `@pytest.mark.parametrize`, реализуйте `to_roman(n: int) -> str`, конвертирующий положительное целое число в строку римских цифр.
**Шаг 1 -- Красный:** Параметризованный тест уже написан ниже. Запустите его -- все 10 кейсов падают, потому что `to_roman` возвращает `None`. Ничего не реализуйте пока. Прочитайте все 10 кейсов, чтобы понять полный контракт.
**Шаг 2 -- Зелёный:** Реализуйте `to_roman`. Чистый подход: список пар `(value, numeral)` в убывающем порядке, включая субтрактивные случаи (`900 -> "CM"`, `400 -> "CD"`, `90 -> "XC"` и т.д.). Цикл: пока `n >= value`, добавлять цифру и вычитать значение.
**Шаг 3 -- Рефакторинг:** Когда все 10 кейсов проходят, проверьте реализацию. Таблица поиска в правильном порядке? Все субтрактивные пары присутствуют?
```python
# test_roman.py -- спецификация уже написана; запустите как есть
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
```
# roman.py
def to_roman(n: int) -> str:
pass
# test_roman.py -- не изменять; запустить pytest и наблюдать как все 10 падают
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
Решение
# roman.py
def to_roman(n: int) -> str:
values = [
(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
(100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
(1, "I"),
]
result = ""
for value, numeral in values:
while n >= value:
result += numeral
n -= value
return result
# test_roman.py -- без изменений
import pytest
from roman import to_roman
@pytest.mark.parametrize("n,expected", [
(1, "I"),
(4, "IV"),
(9, "IX"),
(14, "XIV"),
(40, "XL"),
(90, "XC"),
(400, "CD"),
(900, "CM"),
(1994, "MCMXCIV"),
(2024, "MMXXIV"),
])
def test_to_roman(n, expected):
assert to_roman(n) == expected
No split tab
Настройки cookies
Мы используем необходимые cookies для работы сайта. С вашего разрешения мы также можем сохранять настройки сайта и использовать аналитические и рекламные cookies, чтобы понимать использование сайта и поддерживать развитие проекта.
* Вы всегда можете изменить свой выбор в настройках сайта.
Выберите категории cookies
Настройки аналитики
Можно отключить аналитику использования платформы. Также можно отправить в Google Analytics запрос на удаление данных об использовании этого сайта, связанных с этим браузером.
Учебный workspace
Учитесь, читая, запуская код и решая задачи.
Практикуйте программирование с пояснениями тем, упражнениями, инструментами browser IDE, проверкой regex и тренировкой печати кода в одном workspace.
Открывайте инструменты во вкладках.Упражнения, IDE-инструменты и тренажеры остаются доступными как вкладки сайта.
Переключайтесь без потери контекста.Переходите между пояснениями, кодом и утилитами, сохраняя свое место.
Используйте sidebar как карту.Левые панели содержат навигацию, настройки, файлы, libraries и управление инструментами.
PythonJavaScriptSQLite
Одна IDE, три практичных режима
Python в браузере.Запускайте небольшие скрипты, пробуйте библиотеки и тренируйте API-запросы без установки.
JavaScript для быстрых экспериментов.Проверяйте код для браузера и сравнивайте идеи рядом с учебными материалами.
SQLite для практики с данными.Открывайте обозреватель базы данных, изучайте таблицы, пишите запросы и учитесь SQL локально.
ТемаIDE
Работайте рядом в split tabs
Держите инструкции перед глазами.Откройте упражнение или справочную страницу рядом с IDE, вместо постоянных переключений.
Сравнивайте инструменты во время обучения.Размещайте проверки regex, пояснения и эксперименты с кодом рядом, когда это нужно для задачи.
Закройте split, когда закончите.Workspace вернётся к одной сфокусированной вкладке, а открытые вкладки сайта останутся доступны.
Тренажер слепой печати кода
Или просто текста
Тренажер рассчитан на физическую клавиатуру.Откройте этот раздел на ноутбуке или компьютере с широким экраном. На телефоне тренировка слепой печати не будет корректной.
Скорость: 0 зн/мин
0 слов/мин
Лучшая скорость (60с): 0 зн/мин
0 слов/мин
Ошибки: 0
Общее время: 0.0 с
Для активации режима слепого набора не подсматривайте на физическую клавиатуру.