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) | ☐ |