Skip to content

🔄 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 phase

Step 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 GREEN

Step 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:

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

🎉 Module Hoàn thành!

Bạn đã học toàn bộ về Production Quality Assurance:

  1. 🏗️ Testing Pyramid và GTest setup
  2. 📚 GTest Basics (ASSERT/EXPECT, Fixtures)
  3. 🎭 GMock và Dependency Injection
  4. 🔬 Advanced (TEST_P, Boundary Analysis, Coverage)
  5. 🔄 TDD Mindset (Red → Green → Refactor)

Bước tiếp theo: Quay lại C++ Roadmap để tiếp tục học các modules khác!