Test-Driven Development với Claude Code — Viết test trước, code sau
Điểm nổi bật
Nhấn để đến mục tương ứng
- 1 Mã giảm giá không được giảm quá 50% tổng đơn Hãy viết test cases bằng pytest cho module này TRƯỚC.
- 2 Áp dụng 2 mã giảm giá liên tiếp Mỗi test case cần docstring giải thích tại sao edge case này quan trọng.
- 3 Tất cả tests phải pass sau mỗi bước refactor Tips thực hành TDD với Claude Code Test naming convention: Yêu cầu Claude Code dùng format "test_should_[expected behavior]_when_[condition]" để test names tự giải thích Arrange-Act-Assert: Mỗi test nên theo cấu trúc rõ ràng: setup, execute, verify.
- 4 Refactor: Cải thiện code trong khi giữ tất cả tests pass.
- 5 Khi bạn yêu cầu Claude Code "viết hàm tính giá sau discount", kết quả phụ thuộc vào cách Claude hiểu yêu cầu.
Test-Driven Development (TDD) là phương pháp phát triển phần mềm nơi bạn viết test trước khi viết code. Nghe có vẻ ngược đời, nhưng khi kết hợp với Claude Code, TDD trở thành một workflow cực kỳ hiệu quả: bạn mô tả yêu cầu dưới dạng test cases, Claude Code viết code để pass tất cả tests, và bạn có một safety net hoàn chỉnh cho mọi lần refactoring sau này. Bài viết này hướng dẫn chi tiết cách áp dụng TDD với Claude Code qua các ví dụ thực tế với pytest và Jest.
TDD là gì và tại sao nên dùng với Claude Code?
TDD tuân theo chu trình Red-Green-Refactor:
- Red: Viết test mô tả hành vi mong muốn. Chạy test -- test sẽ fail vì chưa có code.
- Green: Viết code tối thiểu để pass test. Không cần code đẹp, chỉ cần pass.
- Refactor: Cải thiện code trong khi giữ tất cả tests pass. Đây là lúc bạn làm code sạch và tối ưu.
Khi kết hợp với Claude Code, mỗi bước trở nên nhanh hơn:
- Red: Bạn viết test (hoặc nhờ Claude Code viết test từ requirements). Test fail xác nhận rằng test đang kiểm tra đúng thứ.
- Green: Claude Code đọc test, hiểu expected behavior, và sinh code implementation. Vì test là specification rõ ràng nhất, Claude Code có context chính xác để viết code đúng.
- Refactor: Bạn yêu cầu Claude Code refactor, rồi chạy lại tests để đảm bảo không break gì.
Tại sao TDD + Claude Code hiệu quả hơn cách truyền thống?
Khi bạn yêu cầu Claude Code "viết hàm tính giá sau discount", kết quả phụ thuộc vào cách Claude hiểu yêu cầu. Nhưng khi bạn cung cấp test cases cụ thể, Claude Code có specification chính xác. Test case chính là dạng tốt nhất của requirement documentation -- không mơ hồ, có thể verify tự động.
Workflow TDD với Claude Code
Bước 1: Mô tả requirements cho Claude Code, yêu cầu sinh tests
Prompt trong Claude Code terminal:
Tôi cần xây dựng module quản lý giỏ hàng (shopping cart)
với các yêu cầu sau:
1. Thêm sản phẩm vào giỏ (tên, giá, số lượng)
2. Xóa sản phẩm khỏi giỏ
3. Cập nhật số lượng sản phẩm
4. Tính tổng tiền
5. Áp dụng mã giảm giá (% hoặc số tiền cố định)
6. Giỏ hàng không được có sản phẩm với số lượng <= 0
7. Mã giảm giá không được giảm quá 50% tổng đơn
Hãy viết test cases bằng pytest cho module này TRƯỚC.
Chưa viết implementation. Tổ chức tests theo từng feature.
Bước 2: Claude Code sinh test file
Claude Code sẽ tạo file test với cấu trúc rõ ràng:
# test_cart.py
import pytest
from cart import ShoppingCart, Product, DiscountCode
class TestAddProduct:
def test_add_single_product(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
assert len(cart.items) == 1
assert cart.items[0].name == "Áo thun"
assert cart.items[0].price == 250000
assert cart.items[0].quantity == 1
def test_add_multiple_products(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
cart.add_product("Quần jeans", price=450000, quantity=2)
assert len(cart.items) == 2
def test_add_existing_product_increases_quantity(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
cart.add_product("Áo thun", price=250000, quantity=2)
assert len(cart.items) == 1
assert cart.items[0].quantity == 3
def test_add_product_with_zero_quantity_raises(self):
cart = ShoppingCart()
with pytest.raises(ValueError, match="phải lớn hơn 0"):
cart.add_product("Áo thun", price=250000, quantity=0)
def test_add_product_with_negative_quantity_raises(self):
cart = ShoppingCart()
with pytest.raises(ValueError, match="phải lớn hơn 0"):
cart.add_product("Áo thun", price=250000, quantity=-1)
class TestRemoveProduct:
def test_remove_existing_product(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
cart.remove_product("Áo thun")
assert len(cart.items) == 0
def test_remove_nonexistent_product_raises(self):
cart = ShoppingCart()
with pytest.raises(KeyError):
cart.remove_product("Sản phẩm không tồn tại")
class TestUpdateQuantity:
def test_update_quantity(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
cart.update_quantity("Áo thun", 5)
assert cart.items[0].quantity == 5
def test_update_to_zero_removes_product(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=3)
cart.update_quantity("Áo thun", 0)
assert len(cart.items) == 0
def test_update_negative_quantity_raises(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
with pytest.raises(ValueError):
cart.update_quantity("Áo thun", -1)
class TestCalculateTotal:
def test_total_single_product(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=2)
assert cart.calculate_total() == 500000
def test_total_multiple_products(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=2)
cart.add_product("Quần jeans", price=450000, quantity=1)
assert cart.calculate_total() == 950000
def test_total_empty_cart(self):
cart = ShoppingCart()
assert cart.calculate_total() == 0
class TestDiscountCode:
def test_percentage_discount(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=2)
cart.apply_discount(DiscountCode("SALE20", discount_type="percent", value=20))
assert cart.calculate_total() == 400000 # 500000 * 0.8
def test_fixed_amount_discount(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=2)
cart.apply_discount(DiscountCode("MINUS100K", discount_type="fixed", value=100000))
assert cart.calculate_total() == 400000 # 500000 - 100000
def test_discount_cannot_exceed_50_percent(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
with pytest.raises(ValueError, match="không được giảm quá 50%"):
cart.apply_discount(DiscountCode("BIG60", discount_type="percent", value=60))
def test_fixed_discount_cannot_exceed_50_percent_of_total(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=250000, quantity=1)
with pytest.raises(ValueError, match="không được giảm quá 50%"):
cart.apply_discount(DiscountCode("BIG", discount_type="fixed", value=200000))
def test_total_never_goes_negative(self):
cart = ShoppingCart()
cart.add_product("Áo thun", price=100000, quantity=1)
cart.apply_discount(DiscountCode("MINUS50K", discount_type="fixed", value=50000))
assert cart.calculate_total() >= 0
Bước 3: Chạy tests -- xác nhận tất cả FAIL (Red)
# Trong terminal Claude Code
pytest test_cart.py -v
# Kết quả mong đợi: tất cả tests FAIL vì chưa có file cart.py
# FAILED test_cart.py::TestAddProduct::test_add_single_product - ModuleNotFoundError
# ...
# 15 failed
Đây chính là bước Red. Tests fail vì module cart chưa tồn tại. Điều này quan trọng: nó xác nhận tests đang kiểm tra đúng thứ (nếu test pass mà chưa có code, test có vấn đề).
Bước 4: Yêu cầu Claude Code viết implementation (Green)
Đọc file test_cart.py và viết implementation trong cart.py
để tất cả tests pass. Chỉ viết code tối thiểu cần thiết.
Claude Code sẽ đọc tests, hiểu specification, và sinh code. Sau đó chạy tests lại:
pytest test_cart.py -v
# Kết quả mong đợi: tất cả tests PASS
# PASSED test_cart.py::TestAddProduct::test_add_single_product
# PASSED test_cart.py::TestAddProduct::test_add_multiple_products
# ...
# 15 passed
Bước 5: Refactor với safety net (Refactor)
Refactor cart.py để:
1. Tách Product và DiscountCode thành file models.py
2. Thêm type hints đầy đủ
3. Tối ưu performance cho calculate_total (dùng cached property)
4. Đảm bảo tất cả tests vẫn pass sau refactoring
Claude Code refactor code, rồi chạy tests để xác nhận không break gì. Nếu có test fail, Claude Code biết chính xác chỗ nào bị ảnh hưởng và sửa ngay.
TDD với Jest cho TypeScript/JavaScript
Quy trình tương tự áp dụng cho dự án TypeScript/JavaScript với Jest:
Prompt sinh tests:
Tôi cần module xử lý đặt lịch hẹn (appointment booking) với yêu cầu:
1. Tạo appointment với ngày, giờ, duration, tên khách
2. Không cho đặt trùng thời gian (overlap detection)
3. Chỉ đặt được trong giờ làm việc (8:00-17:00)
4. Duration tối thiểu 30 phút, tối đa 4 giờ
5. Hủy appointment phải trước ít nhất 2 giờ
6. Reschedule giữ nguyên appointment ID
Viết test cases bằng Jest + TypeScript. Chưa viết implementation.
Claude Code sinh test file:
// appointment.test.ts
import { AppointmentManager, Appointment } from "./appointment";
describe("AppointmentManager", () => {
let manager: AppointmentManager;
beforeEach(() => {
manager = new AppointmentManager();
});
describe("createAppointment", () => {
it("should create appointment with valid data", () => {
const apt = manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 60,
customerName: "Nguyen Van A",
});
expect(apt.id).toBeDefined();
expect(apt.customerName).toBe("Nguyen Van A");
expect(apt.duration).toBe(60);
});
it("should reject appointment outside business hours", () => {
expect(() =>
manager.createAppointment({
date: "2026-04-01",
startTime: "07:00",
duration: 60,
customerName: "Nguyen Van A",
})
).toThrow("ngoài giờ làm việc");
});
it("should reject appointment ending after business hours", () => {
expect(() =>
manager.createAppointment({
date: "2026-04-01",
startTime: "16:30",
duration: 60,
customerName: "Nguyen Van A",
})
).toThrow("ngoài giờ làm việc");
});
it("should reject duration less than 30 minutes", () => {
expect(() =>
manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 15,
customerName: "Nguyen Van A",
})
).toThrow("Duration");
});
it("should reject duration more than 4 hours", () => {
expect(() =>
manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 300,
customerName: "Nguyen Van A",
})
).toThrow("Duration");
});
});
describe("overlap detection", () => {
it("should reject overlapping appointments", () => {
manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 60,
customerName: "Nguyen Van A",
});
expect(() =>
manager.createAppointment({
date: "2026-04-01",
startTime: "09:30",
duration: 60,
customerName: "Tran Van B",
})
).toThrow("trùng lịch");
});
it("should allow back-to-back appointments", () => {
manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 60,
customerName: "Nguyen Van A",
});
const apt = manager.createAppointment({
date: "2026-04-01",
startTime: "10:00",
duration: 60,
customerName: "Tran Van B",
});
expect(apt.id).toBeDefined();
});
it("should allow same time different dates", () => {
manager.createAppointment({
date: "2026-04-01",
startTime: "09:00",
duration: 60,
customerName: "Nguyen Van A",
});
const apt = manager.createAppointment({
date: "2026-04-02",
startTime: "09:00",
duration: 60,
customerName: "Tran Van B",
});
expect(apt.id).toBeDefined();
});
});
describe("cancelAppointment", () => {
it("should cancel appointment at least 2 hours before", () => {
const apt = manager.createAppointment({
date: "2026-04-10",
startTime: "14:00",
duration: 60,
customerName: "Nguyen Van A",
});
// Giả lập thời điểm hiện tại là 11:00 ngày 10/04
const result = manager.cancelAppointment(
apt.id,
new Date("2026-04-10T11:00:00")
);
expect(result.status).toBe("cancelled");
});
it("should reject cancellation less than 2 hours before", () => {
const apt = manager.createAppointment({
date: "2026-04-10",
startTime: "14:00",
duration: 60,
customerName: "Nguyen Van A",
});
expect(() =>
manager.cancelAppointment(
apt.id,
new Date("2026-04-10T12:30:00")
)
).toThrow("trước ít nhất 2 giờ");
});
});
describe("rescheduleAppointment", () => {
it("should keep same ID after reschedule", () => {
const apt = manager.createAppointment({
date: "2026-04-10",
startTime: "09:00",
duration: 60,
customerName: "Nguyen Van A",
});
const rescheduled = manager.rescheduleAppointment(apt.id, {
date: "2026-04-11",
startTime: "10:00",
});
expect(rescheduled.id).toBe(apt.id);
expect(rescheduled.date).toBe("2026-04-11");
expect(rescheduled.startTime).toBe("10:00");
});
});
});
Sau khi có tests, yêu cầu Claude Code viết implementation:
Đọc file appointment.test.ts và viết implementation
trong appointment.ts để tất cả tests pass.
Dùng TypeScript strict mode, có type definitions đầy đủ.
Coverage tracking
TDD tự nhiên tạo ra test coverage cao, nhưng vẫn cần theo dõi để phát hiện gaps:
# Python - pytest-cov
pytest test_cart.py --cov=cart --cov-report=term-missing
# JavaScript - Jest coverage
npx jest --coverage
Yêu cầu Claude Code bổ sung tests cho uncovered paths:
Chạy pytest --cov=cart --cov-report=term-missing và xem kết quả.
Viết thêm test cases cho những dòng code chưa được cover.
Target: 95% coverage.
TDD cho edge cases và error handling
Một lợi thế lớn của TDD với Claude Code là khả năng sinh edge case tests mà bạn có thể bỏ sót:
Đọc implementation hiện tại trong cart.py và viết thêm edge case tests:
1. Concurrent modifications (nếu applicable)
2. Unicode trong tên sản phẩm
3. Số tiền rất lớn (overflow check)
4. Empty string cho tên sản phẩm
5. Floating point precision cho giá tiền
6. Áp dụng 2 mã giảm giá liên tiếp
Mỗi test case cần docstring giải thích tại sao edge case này quan trọng.
Refactoring an toàn với test safety net
Đây là lúc giá trị của TDD thực sự tỏa sáng. Khi có đủ tests, bạn có thể yêu cầu Claude Code refactor mạnh tay mà không sợ break functionality:
Ví dụ refactoring scenarios:
# Scenario 1: Tách module
Refactor cart.py thành cấu trúc module:
- models/product.py (Product dataclass)
- models/discount.py (DiscountCode class)
- services/cart_service.py (ShoppingCart logic)
- services/pricing_service.py (tính giá logic)
Cập nhật imports trong test_cart.py.
Chạy tests để xác nhận không break gì.
# Scenario 2: Đổi data structure
Hiện tại items dùng list. Đổi sang dict với product name
làm key để tối ưu lookup performance.
Giữ API bên ngoài không đổi.
Chạy tests sau khi refactor.
# Scenario 3: Thêm persistence
Thêm khả năng serialize/deserialize cart sang JSON.
Viết tests trước cho serialize/deserialize,
rồi implement.
Tests cũ phải vẫn pass.
Real project walkthrough: API endpoint với TDD
Dưới đây là walkthrough đầy đủ cho một tình huống thực tế: xây dựng API endpoint quản lý todo list.
Phase 1: Viết API tests trước
Tôi cần xây dựng REST API cho todo list với FastAPI:
- GET /todos - Lấy danh sách todos (hỗ trợ filter by status)
- POST /todos - Tạo todo mới
- PUT /todos/{id} - Cập nhật todo
- DELETE /todos/{id} - Xóa todo
- PATCH /todos/{id}/complete - Đánh dấu hoàn thành
Business rules:
- Title không được rỗng, tối đa 200 ký tự
- Không xóa được todo đã hoàn thành
- Completed todo không thể chuyển về incomplete
Viết integration tests bằng pytest + httpx cho FastAPI TestClient.
Chưa viết API code.
Phase 2: Implement từng endpoint
Thay vì implement tất cả cùng lúc, áp dụng TDD incremental:
# Lần 1: Chỉ implement POST /todos
# Chạy tests -> chỉ test POST pass, còn lại fail (expected)
# Lần 2: Implement GET /todos
# Chạy tests -> POST + GET pass
# Lần 3: Implement PUT, DELETE, PATCH
# Chạy tests -> tất cả pass
Phase 3: Refactor với confidence
Refactor todo API:
1. Tách route handlers ra khỏi business logic
2. Thêm dependency injection cho database layer
3. Implement repository pattern
4. Tất cả tests phải pass sau mỗi bước refactor
Tips thực hành TDD với Claude Code
- Test naming convention: Yêu cầu Claude Code dùng format "test_should_[expected behavior]_when_[condition]" để test names tự giải thích
- Arrange-Act-Assert: Mỗi test nên theo cấu trúc rõ ràng: setup, execute, verify. Yêu cầu Claude Code tách 3 phần bằng comment hoặc blank lines
- Một test, một assertion: Mỗi test nên kiểm tra một behavior cụ thể. Nếu test fail, bạn biết ngay chính xác cái gì hỏng
- Test independence: Tests không được phụ thuộc nhau. Dùng setup/teardown (beforeEach, setUp) để đảm bảo mỗi test bắt đầu từ clean state
- Đừng test implementation: Test behavior, không test cách code được viết. Điều này cho phép refactor thoải mái mà không phải sửa tests
- Commit sau mỗi Green: Sau khi tests pass, commit ngay. Điều này cho phép bạn rollback bất kỳ lúc nào về trạng thái "all tests pass"
CLAUDE.md configuration cho TDD workflow
Cấu hình CLAUDE.md để Claude Code tự động chạy tests sau mỗi lần sửa code:
# CLAUDE.md
## Testing
- Luôn chạy tests sau khi sửa code
- Python: pytest -v --tb=short
- JavaScript: npx jest --verbose
- Target coverage: 90%+
- Viết test trước khi viết implementation
- Mỗi function phải có ít nhất: happy path test,
error case test, edge case test
Kết hợp TDD với Claude Code Plan Mode
Với tính năng Plan Mode của Claude Code, bạn có thể lên kế hoạch TDD cho cả feature lớn:
Plan mode: Lên kế hoạch TDD cho tính năng "user authentication":
1. Liệt kê tất cả components cần implement
2. Với mỗi component, liệt kê test cases cần viết
3. Sắp xếp thứ tự implement (dependency order)
4. Estimate số test cases và LOC cho mỗi component
Chưa viết code, chỉ planning.
Tổng kết
TDD với Claude Code tạo ra một vòng phản hồi nhanh và đáng tin cậy. Tests trở thành specification sống -- luôn đúng, luôn được cập nhật, và luôn có thể verify tự động. Bạn viết requirements dưới dạng tests, Claude Code biến tests thành implementation, và cả hai cùng đảm bảo chất lượng qua mỗi iteration. Bắt đầu bằng một module nhỏ, làm quen với chu trình Red-Green-Refactor, và dần áp dụng cho toàn bộ dự án. Khi đã có thói quen, bạn sẽ không muốn quay lại cách viết code trước rồi viết test sau nữa.
Bai viet co huu ich khong?
Bản quyền thuộc về tác giả. Vui lòng dẫn nguồn khi chia sẻ.





