TDD Cơ bản — Red-Green-Refactor
"The secret to getting good at TDD is to do it. A lot. On small things. Until it becomes reflex." — Kent Beck
Bài này là một TDD session thực chiến — chúng ta sẽ xây dựng một String Calculator từ đầu đến cuối, theo đúng chu kỳ Red-Green-Refactor, từng bước một. Mỗi bước đều có code, test, và kết quả chạy. Bạn có thể chạy theo trên máy của mình.
Bài toán: String Calculator
String Calculator là một classic TDD kata (bài tập luyện) do Roy Osherove phổ biển. Yêu cầu:
- Calculator nhận một string gồm các số phân cách bởi dấu phẩy, trả về tổng
- Hỗ trợ số lượng số không giới hạn
- Hỗ trợ newline làm delimiter
- Hỗ trợ custom delimiter (dạng
//[delimiter]\n[numbers]) - Ném exception khi gặp số âm — liệt kê tất cả số âm trong message
- Bỏ qua số >1000
- Delimiter có thể dài hơn 1 ký tự
- Hỗ trợ multiple delimiters
Chúng ta sẽ implement từng yêu cầu m ột, theo TDD cycle.
Cấu trúc project
string_calculator/
├── src/
│ ├── __init__.py
│ └── calculator.py
├── tests/
│ ├── __init__.py
│ ├── conftest.py
│ └── test_calculator.py
└── pyproject.toml
Bước 1: RED — Test cơ bản đầu tiên
Chúng ta chưa có file calculator.py. Hãy viết test trước:
# tests/test_calculator.py
from src.calculator import StringCalculator
class TestStringCalculator:
def test_add_returns_0_for_empty_string(self):
calc = StringCalculator()
assert calc.add("") == 0
Chạy test:
$ pytest -v
============================= test session starts =============================
collected 1 item
tests/test_calculator.py FAILED [100%]
================================== FAILURES ===================================
___________ TestStringCalculator.test_add_returns_0_for_empty_string __________
...
ModuleNotFoundError: No module named 'src.calculator'
RED — như mong đợi.
Bước 2: GREEN — Code tối thiểu
# src/calculator.py
class StringCalculator:
def add(self, numbers: str) -> int:
return 0
Chạy lại:
$ pytest -v
============================= test session starts =============================
collected 1 item
tests/test_calculator.py PASSED [100%]
============================== 1 passed in 0.02s ==============================
GREEN. Code tối thiểu — chỉ return 0 — vì test chưa yêu cầu gì hơn.
Bước 3: RED — Test với một số
def test_add_returns_the_number_itself_for_single_number(self):
calc = StringCalculator()
assert calc.add("1") == 1
Chạy:
$ pytest -v
...
FAILED — assert 0 == 1
RED.
Bước 4: GREEN — Mở rộng code
class StringCalculator:
def add(self, numbers: str) -> int:
if numbers == "":
return 0
return int(numbers)
Chạy: GREEN. Cả 2 test pass.
Bước 5: REFACTOR
Code vẫn đơn giản, chưa cần refactor. Hãy thêm type hints:
class StringCalculator:
def add(self, numbers: str) -> int:
if not numbers:
return 0
return int(numbers)
Test vẫn xanh.
Bước 6: RED — Test với hai số
def test_add_returns_sum_for_two_numbers(self):
calc = StringCalculator()
assert calc.add("1,2") == 3
RED: ValueError: invalid literal for int() with base 10: '1,2'
Bước 7: GREEN — Parse delimiter
class StringCalculator:
def add(self, numbers: str) -> int:
if not numbers:
return 0
parts = numbers.split(",")
return sum(int(n) for n in parts)
GREEN.