Giao diện
🔄 Test-Driven Development Philosophy
Red → Green → Refactor — Một trong những practices quan trọng nhất của Software Engineering hiện đại.
TDD là gì?
Test-Driven Development — Viết test TRƯỚC khi viết implementation.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE TDD CYCLE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────┐ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ RED │ Write a failing test │ │
│ │ (Test fails) │ │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ GREEN │ Write minimum code │ │
│ │ (Test passes) │ to make test pass │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ▼ │ │
│ ┌─────────────────┐ │ │
│ │ REFACTOR │ Improve code quality │ │
│ │ (Still passes) │ (Keep tests green) │ │
│ └────────┬────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────┘ │
│ │
│ Cycle Time: 2-10 minutes per cycle │
│ │
└─────────────────────────────────────────────────────────────────────────┘TDD Exercise: Building a Stack
Step 1: RED — Write First Failing Test
cpp
// stack_test.cpp
#include <gtest/gtest.h>
#include "stack.hpp"
TEST(StackTest, NewStackIsEmpty) {
Stack<int> stack;
EXPECT_TRUE(stack.IsEmpty());
}bash
# Compile: ERROR - Stack doesn't exist yet!
# This is expected - we're in RED phaseStep 2: GREEN — Write Minimum Code
cpp
// stack.hpp
#pragma once
template<typename T>
class Stack {
public:
bool IsEmpty() const { return true; } // Simplest possible!
};bash
# Run test: PASS ✅
# We're back to GREENStep 3: REFACTOR — Nothing to refactor yet
Step 4: RED — Next Test
cpp
TEST(StackTest, PushMakesStackNonEmpty) {
Stack<int> stack;
stack.Push(42);
EXPECT_FALSE(stack.IsEmpty()); // FAILS! IsEmpty() always returns true
}Step 5: GREEN — Implement Push
cpp
template<typename T>
class Stack {
public:
void Push(T value) {
data_.push_back(std::move(value));
}
bool IsEmpty() const {
return data_.empty();
}
private:
std::vector<T> data_;
};Step 6: Continue TDD Cycle
cpp
// More tests...
TEST(StackTest, PopReturnsLastPushedValue) {
Stack<int> stack;
stack.Push(42);
EXPECT_EQ(stack.Pop(), 42);
}
TEST(StackTest, PopRemovesValue) {
Stack<int> stack;
stack.Push(42);
stack.Pop();
EXPECT_TRUE(stack.IsEmpty());
}
TEST(StackTest, PopOnEmptyStackThrows) {
Stack<int> stack;
EXPECT_THROW(stack.Pop(), std::runtime_error);
}
TEST(StackTest, LIFO_Order) {
Stack<int> stack;
stack.Push(1);
stack.Push(2);
stack.Push(3);
EXPECT_EQ(stack.Pop(), 3);
EXPECT_EQ(stack.Pop(), 2);
EXPECT_EQ(stack.Pop(), 1);
}Final Implementation
cpp
// stack.hpp
#pragma once
#include <vector>
#include <stdexcept>
template<typename T>
class Stack {
public:
void Push(T value) {
data_.push_back(std::move(value));
}
T Pop() {
if (IsEmpty()) {
throw std::runtime_error("Cannot pop from empty stack");
}
T value = std::move(data_.back());
data_.pop_back();
return value;
}
const T& Top() const {
if (IsEmpty()) {
throw std::runtime_error("Cannot top on empty stack");
}
return data_.back();
}
bool IsEmpty() const {
return data_.empty();
}
size_t Size() const {
return data_.size();
}
private:
std::vector<T> data_;
};TDD Benefits
┌─────────────────────────────────────────────────────────────────────────┐
│ WHY TDD WORKS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. FORCES TESTABLE DESIGN │
│ ───────────────────────── │
│ • You MUST write testable code to pass tests │
│ • Naturally leads to Dependency Injection │
│ • Small, focused classes and functions │
│ │
│ 2. DOCUMENTATION THROUGH TESTS │
│ ──────────────────────────── │
│ • Tests show HOW to use the API │
│ • Tests define WHAT the code should do │
│ • Always up-to-date (unlike comments) │
│ │
│ 3. CONFIDENCE TO REFACTOR │
│ ────────────────────────── │
│ • Tests catch breaking changes immediately │
│ • Can aggressively improve code quality │
│ • No fear of "I might break something" │
│ │
│ 4. FEWER BUGS IN PRODUCTION │
│ ──────────────────────────── │
│ • Bugs caught before code is written │
│ • Edge cases considered upfront │
│ • Regression tests built automatically │
│ │
└─────────────────────────────────────────────────────────────────────────┘TDD Example: Banking Transfer (@[/deep-test])
Requirements
- Transfer money between two accounts
- Validate sufficient balance
- Validate positive amount
- Both accounts must exist
TDD Implementation
cpp
// Step 1: RED - Basic transfer
TEST(TransferTest, TransfersMoney) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
EXPECT_CALL(*mock_db, GetBalance(1)).WillOnce(Return(100.0));
EXPECT_CALL(*mock_db, UpdateBalance(1, 50.0)); // Debit
EXPECT_CALL(*mock_db, UpdateBalance(2, 50.0)); // Credit (assuming 0)
auto result = service.Transfer(1, 2, 50.0);
EXPECT_TRUE(result.ok());
}
// Step 2: RED - Insufficient funds
TEST(TransferTest, RejectsInsufficientFunds) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
EXPECT_CALL(*mock_db, GetBalance(1)).WillOnce(Return(30.0));
EXPECT_CALL(*mock_db, UpdateBalance(_, _)).Times(0); // No updates!
auto result = service.Transfer(1, 2, 50.0);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.error(), TransferError::InsufficientFunds);
}
// Step 3: RED - Invalid amount
TEST(TransferTest, RejectsNegativeAmount) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
EXPECT_CALL(*mock_db, GetBalance(_)).Times(0); // Not even checked
auto result = service.Transfer(1, 2, -10.0);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.error(), TransferError::InvalidAmount);
}
// Step 4: RED - Zero amount
TEST(TransferTest, RejectsZeroAmount) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
auto result = service.Transfer(1, 2, 0.0);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.error(), TransferError::InvalidAmount);
}
// Step 5: RED - Same account
TEST(TransferTest, RejectsSameSourceAndDest) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
auto result = service.Transfer(1, 1, 50.0);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.error(), TransferError::SameAccount);
}
// Step 6: RED - Account not found
TEST(TransferTest, RejectsNonExistentAccount) {
auto mock_db = std::make_shared<MockDatabase>();
TransferService service(mock_db);
EXPECT_CALL(*mock_db, GetBalance(999))
.WillOnce(Return(std::nullopt)); // Account doesn't exist
auto result = service.Transfer(999, 2, 50.0);
EXPECT_FALSE(result.ok());
EXPECT_EQ(result.error(), TransferError::AccountNotFound);
}Implementation (after all tests written)
cpp
class TransferService {
public:
explicit TransferService(std::shared_ptr<IDatabase> db) : db_(db) {}
Result<void, TransferError> Transfer(int from, int to, double amount) {
// Validation first
if (amount <= 0) {
return TransferError::InvalidAmount;
}
if (from == to) {
return TransferError::SameAccount;
}
// Check source account
auto balance = db_->GetBalance(from);
if (!balance.has_value()) {
return TransferError::AccountNotFound;
}
if (*balance < amount) {
return TransferError::InsufficientFunds;
}
// Execute transfer
db_->UpdateBalance(from, *balance - amount);
auto dest_balance = db_->GetBalance(to).value_or(0.0);
db_->UpdateBalance(to, dest_balance + amount);
return {}; // Success
}
private:
std::shared_ptr<IDatabase> db_;
};When to Use TDD
┌─────────────────────────────────────────────────────────────────────────┐
│ WHEN TO USE TDD │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ GOOD FIT FOR TDD: │
│ ───────────────────── │
│ • Business logic (banking, payments, auth) │
│ • Data transformations │
│ • Algorithm implementations │
│ • API contracts │
│ • Bug fixes (write test that reproduces bug first) │
│ │
│ ⚠️ LESS IDEAL FOR TDD: │
│ ──────────────────────── │
│ • Exploratory/prototype code │
│ • UI/Visual code │
│ • Integration with external systems │
│ • Performance-critical hot paths (profile first) │
│ │
│ 💡 HYBRID APPROACH: │
│ ──────────────────── │
│ 1. Spike/prototype without tests │
│ 2. Once design is clear, add tests │
│ 3. Use TDD for subsequent features │
│ │
└─────────────────────────────────────────────────────────────────────────┘TDD Anti-Patterns
┌─────────────────────────────────────────────────────────────────────────┐
│ TDD ANTI-PATTERNS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ❌ THE GIANT LEAP │
│ ───────────────── │
│ Writing too much code at once │
│ → Keep cycles to 2-10 minutes │
│ │
│ ❌ THE SKIPPER │
│ ───────────────── │
│ Skipping the refactor step │
│ → Technical debt accumulates │
│ │
│ ❌ THE GOLDPLATER │
│ ─────────────────── │
│ Adding features not required by tests │
│ → YAGNI (You Ain't Gonna Need It) │
│ │
│ ❌ THE MOCKER │
│ ───────────────── │
│ Mocking everything, including the unit under test │
│ → Tests become useless │
│ │
│ ❌ THE IGNORER │
│ ─────────────────── │
│ Ignoring failing tests "for now" │
│ → Broken window effect │
│ │
└─────────────────────────────────────────────────────────────────────────┘Self-Review Checklist (@[/review])
After completing TDD, review your code:
| Criteria | Check |
|---|---|
| Tests are readable (good names) | ☐ |
| Each test tests ONE thing | ☐ |
| No test depends on another | ☐ |
| Tests run in < 1 second total | ☐ |
| No hardcoded values (use constants) | ☐ |
| Error messages are clear | ☐ |
| Edge cases covered | ☐ |
| Implementation matches tests (no extra code) | ☐ |
🧠 Quiz
Câu 1: Trong TDD, thứ tự đúng của chu trình phát triển là gì?
- [ ] A) Green → Red → Refactor
- [x] B) Red → Green → Refactor
- [ ] C) Refactor → Red → Green
- [ ] D) Red → Refactor → Green
💡 Giải thích: Red: Viết test cho feature chưa tồn tại — test PHẢI fail (đỏ). Green: Viết code tối thiểu nhất để test pass (xanh) — không cần đẹp, chỉ cần đúng. Refactor: Cải thiện code quality mà không thay đổi behavior — test vẫn pass. Chu trình này lặp lại cho mỗi requirement nhỏ, đảm bảo mọi dòng code đều có test coverage.
Câu 2: Trong phase "Red" của TDD, developer nên làm gì?
- [ ] A) Viết production code để pass test hiện có
- [x] B) Viết test cho functionality chưa tồn tại — test phải fail
- [ ] C) Refactor và clean up code hiện có
- [ ] D) Fix bugs được phát hiện trong quá trình testing
💡 Giải thích: Phase Red là bước quan trọng nhất — bạn viết test MÔ TẢ behavior mong muốn TRƯỚC khi viết code. Test fail chứng minh: (1) test thực sự kiểm tra điều gì đó, (2) feature chưa tồn tại. Nếu test pass ngay từ đầu, hoặc test sai, hoặc feature đã có — cả hai đều cần xem xét lại.
Câu 3: Khi nào KHÔNG nên áp dụng TDD?
- [ ] A) Khi viết business logic phức tạp với nhiều edge cases
- [x] B) Khi prototyping nhanh hoặc viết exploratory/throwaway code
- [ ] C) Khi làm việc trong team lớn cần code review
- [ ] D) Khi dự án yêu cầu high reliability
💡 Giải thích: TDD tốn thời gian upfront — phù hợp cho production code cần reliability và maintainability. Nhưng khi prototyping (thử nghiệm ý tưởng, spike solutions), requirements thay đổi liên tục và code có thể bị bỏ. Áp dụng TDD lúc này gây waste. Quy tắc: prototype without TDD → khi đã rõ design, viết lại production code với TDD.