Розробка через тестування (TDD) перевертає звичайний порядок написання коду: ви пишете тест до написання будь-якого коду реалізації, потім пишете рівно стільки коду, щоб тест пройшов, а потім покращуєте код. Дисципліна охоплена трифазним циклом, що називається Red-Green-Refactor.
## Цикл Red-Green-Refactor
**Red** -- Напишіть тест для поведінки, якої ще не існує. Запустіть його. Він провалюється -- вивід є червоним. Це навмисно і правильно. Тест, що провалюється, до будь-якої реалізації доводить, що ваш тест справді щось тестує. Якщо абсолютно новий тест проходить без жодних змін коду, то або поведінка вже реалізована десь ще, або тест неправильний.
**Green** -- Напишіть мінімальний код, який змушує тест, що провалюється, пройти. Не додавайте нічого зайвого. Ніякого коду "знадобиться пізніше". Ніякого полірування. Єдина мета -- зелений вивід. Пишіть найпростіше, що тільки може спрацювати.
**Refactor** -- Тепер, коли код працює, покращуйте його. Видаляйте дублювання, перейменовуйте для ясності, виділяйте допоміжні функції, реорганізовуйте. Тести вас захищають: якщо рефакторинг щось зламав, ви дізнаєтесь про це негайно. Саме в цій фазі ви пишете якісний код -- а не у фазі Green.
```
[Red] Написати тест, що провалюється, для наступної поведінки
↓
[Green] Написати мінімальний код для його проходження
↓
[Refactor] Покращити код, поки тести залишаються зеленими
↓
[Red] Написати наступний тест, що провалюється ...
```
Кожен цикл короткий -- максимум кілька хвилин. Цикл навмисно маленький. Ви ніколи не накопичуєте великі обсяги неперевіреного коду.
## Навіщо писати тест першим?
Аргумент на користь підходу "тест спочатку" -- це не просто дисципліна, він змінює те, що ви будуєте і як ви будуєте.
**Тести, написані після коду, описують те, що код робить, а не те, що він має робити.** Ви вже знаєте реалізацію, коли пишете тести після факту, тому несвідомо підтверджуєте те, що існує, а не вказуєте те, що було задумано. Граничні випадки, які ви не реалізували, не тестуються, тому що ви не подумали їх протестувати.
**Код, який важко тестувати, сигналізує про проблему дизайну.** Функція, що вимагає 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 формує структуру коду
Код, що виріс через підхід "тест спочатку", схильний набувати певних структурних властивостей -- не через дисципліну, а тому що тести вимагають цього під час розробки:
**Менші функції.** Функція, яка вимагає 8 аргументів або глибокого налаштування для виклику, болісно тестується. Біль з'являється ще до того, як код існує, тому ви робите функцію меншою.
**Явні залежності.** Якщо функції потрібне з'єднання з базою даних, тест має його надати. Шлях найменшого опору -- зробити залежність параметром (впровадження залежностей), а не прихованим імпортом на рівні модуля. TDD робить приховані глобалі та побічно-ефектні імпорти негайно болісними.
**Більше чистих функцій.** Функції, що приймають аргументи і повертають значення -- без спільного стану, без файлового введення-виведення, без мережі -- найлегше тестувати. TDD послідовно підштовхує вас писати їх більше.
**Мінімальні, зв'язні класи.** Клас, що робить забагато, вимагає забагато налаштувань у кожному тесті. Тертя тестів сигналізує про проблему дизайну до того, як вона стала усталеною.
## Поширені заперечення
**"TDD займає вдвічі більше часу."**
Тест все одно треба написати в якийсь момент -- питання лише в тому, коли. Написання його першим спрямовує реалізацію і часто прискорює її. Справжнє поглинання часу -- це подальше налагодження нетестованого коду. TDD переміщує витрати з налагодження на проектування, що є кращим обміном.
**"Я ще не знаю, що будую."**
Вам не потрібно знати весь дизайн наперед. Вам потрібно знати лише наступну поведінку. Напишіть один тест для найменшої наступної речі. TDD є інкрементальним -- ви відкриваєте дизайн через тести, а не до них.
**"Мій кодова база не тестована."**
Нетестований код -- це симптом проблем дизайну: тісне зв'язування, приховані залежності, глобальний стан. TDD, застосований до нового коду, запобігає їх формуванню. Застосований до існуючого коду, TDD діє як запобіжний клапан -- він робить проблеми дизайну видимими як біль тестів, мотивуючи вас їх виправляти.
TDD на практиці: побудова BankAccount крок за кроком
## Застосунок, який ми будуємо
Клас `BankAccount` з методами `deposit`, `withdraw` і `balance`, вирощений за суворим TDD -- по одному тесту за раз, мінімальний код для проходження, потім рефакторинг. Два файли:
```
bank.py # виробничий код -- починається порожнім
test_bank.py # тести -- ростуть по одному тесту за раз
```
На початку обидва файли порожні.
---
## Цикл 1: новий рахунок має нульовий баланс
**Red** -- пишемо перший тест:
```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'
```
Добре -- провалюється. У нас є дозвіл писати код.
**Green** -- абсолютний мінімум:
```python
# bank.py
class BankAccount:
def __init__(self):
self.balance = 0
```
```
1 passed
```
**Refactor** -- поки нічого прибирати.
---
## Цикл 2: поповнення збільшує баланс
**Red:**
```python
def test_deposit_increases_balance():
account = BankAccount()
account.deposit(100)
assert account.balance == 100
```
```
FAILED - AttributeError: 'BankAccount' object has no attribute 'deposit'
```
**Green:**
```python
def deposit(self, amount):
self.balance += amount
```
```
2 passed
```
**Refactor** -- поки нема що дедублювати.
---
## Цикл 3: зняття зменшує баланс
**Red:**
```python
def test_withdraw_decreases_balance():
account = BankAccount()
account.deposit(200)
account.withdraw(50)
assert account.balance == 150
```
**Green:**
```python
def withdraw(self, amount):
self.balance -= amount
```
```
3 passed
```
---
## Цикл 4: овердрафт кидає ValueError
Тут тест змушує нас вказати контракт -- тип помилки, повідомлення -- до того, як існує будь-який код:
**Red:**
```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'>
```
**Green:**
```python
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("Insufficient funds")
self.balance -= amount
```
```
4 passed
```
Повідомлення про помилку `"Insufficient funds"` було визначено в тесті -- специфікація керує деталями реалізації, а не навпаки.
---
## Цикл 5: поповнення має бути позитивною сумою
**Red:**
```python
def test_deposit_rejects_non_positive_amount():
account = BankAccount()
with pytest.raises(ValueError, match="Amount must be positive"):
account.deposit(-50)
```
**Green:**
```python
def deposit(self, amount):
if amount <= 0:
raise ValueError("Amount must be positive")
self.balance += amount
```
```
5 passed
```
---
## Цикл 6: сума зняття також має бути позитивною
Зняття нуля або від'ємного числа непомітно пошкодило б баланс. Тест ловить це ще до того, як це може статися:
**Red:**
```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)
```
**Green:**
```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. [Red] Написати тест, що провалюється, для наступної поведінки
2. [Green] Написати мінімальний код для його проходження -- нічого зайвого
3. [Refactor] Прибрати код, поки всі тести залишаються зеленими
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`. Коли тест провалюється, його ім'я має сказати вам, що зламалось, без читання тіла.
## Перевірка фази Red
Новий тест, що відразу проходить (без змін коду), означає одне з двох:
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: пишіть повну специфікацію (всі тестові випадки) до будь-якої реалізації. Всі випадки провалюються одночасно (Red); одна реалізація змушує їх всіх пройти (Green):
```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 показує кожен випадок окремо -- ви можете відстежувати прогрес через фазу Red, коли випадки стають зеленими по одному.
## Тести-характеристики для застарілого коду
При додаванні тестів до існуючого коду без тестів пишіть "тести-характеристики" -- тести, що документують те, що код зараз робить:
```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
| Помилка | Наслідок | Виправлення |
|---|---|---|
| Написання кількох тестів до будь-якого Green | Каскад збоїв, важко ізолювати | По одному тесту за раз |
| Фаза Green додає більше коду, ніж потрібно | Неспецифікована поведінка без тестів | Зробіть лише тест, що провалюється, зеленим |
| Пропуск фази Refactor | Технічний борг накопичується в кожному циклі | Завжди рефакторте, хоча б коротко |
| Незахоплення Red перед написанням коду | Тест може нічого не тестувати насправді | Завжди спостерігайте, як тест провалюється спочатку |
| Тестування внутрішніх деталей реалізації | Тести ламаються при рефакторингу, а не при зміні поведінки | Тестуйте поведінку (входи/виходи), а не внутрішнє |
| Рефакторинг великими кроками | Важко визначити, який крок зламав тест | Малі кроки, запускайте тести між кожним |
Використовуючи TDD, реалізуйте клас `Stack` з чотирма методами: `push(item)`, `pop()`, `peek()` та `is_empty()`.
**Крок 1 -- Red:** Спочатку заповніть тіла всіх 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` -- push 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 -- Green/Refactor:** Реалізуйте `Stack` по одному методу за раз, запускаючи pytest після кожного додавання.
Використовуючи TDD, реалізуйте `validate_email(s: str) -> bool`.
**Крок 1 -- Red:** Спочатку напишіть тіла всіх 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 -- Green:** Реалізуйте по одному правилу валідації за раз. Після кожного правила запускайте 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` приймає будь-який callable, що повертає поточний час як float. За замовчуванням -- `time.time`. Це робить годинник таким, що можна впроваджувати -- тести передають фіктивний годинник замість того, щоб спати.
**Крок 1 -- Red:** Напишіть всі 5 тестів з фіктивним годинником (без `time.sleep`). Всі провалюються, тому що `RateLimiter` ще не існує.
Тести для написання:
1. `test_first_call_is_always_allowed`
2. `test_calls_within_limit_are_allowed` -- при ліміті=2 два виклики при t=0 обидва `True`
3. `test_calls_exceeding_limit_are_denied` -- при ліміті=2 третій виклик при t=0 є `False`
4. `test_calls_reset_after_one_second` -- після переведення фіктивного часу на 1.0 виклики знову дозволені
5. `test_two_limiters_are_independent` -- вичерпання одного лімітера не впливає на інший
**Крок 2 -- Green/Refactor:** Реалізуйте `RateLimiter` з фіксованим 1-секундним вікном. Вікно скидається, коли `now - window_start >= 1.0`.
Використовуючи TDD з `@pytest.mark.parametrize`, реалізуйте `to_roman(n: int) -> str`, що перетворює ціле позитивне число на рядок римських цифр.
**Крок 1 -- Red:** Параметризований тест вже написаний нижче. Запустіть його -- всі 10 випадків провалюються, тому що `to_roman` повертає `None`. Нічого ще не реалізовуйте. Прочитайте всі 10 випадків, щоб зрозуміти повний контракт.
**Крок 2 -- Green:** Реалізуйте `to_roman`. Чистий підхід: список пар `(значення, цифра)` у спадному порядку, включаючи субтрактивні випадки (`900 → "CM"`, `400 → "CD"`, `90 → "XC"` тощо). Цикл: поки `n >= значення`, додайте цифру і відніміть значення.
**Крок 3 -- Refactor:** Коли всі 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 с
Щоб тренувати сліпий друк, не підглядайте на фізичну клавіатуру.