Skip to content

Part 12: Testing & Mocking Zero Defect

"If it's not tested, it doesn't work." — The Beyoncé Rule of Software Engineering.

Module này chuyển đổi kỹ sư từ "manual console testing" sang "automated CI pipelines" với Google TestGoogle Mock.

The Untestable Code Problem (@[/refactor])

Trước khi học testing, hãy hiểu tại sao code thông thường KHÔNG THỂ TEST ĐƯỢC:

cpp
// ❌ UNTESTABLE CODE — Tightly Coupled
class PaymentService {
public:
    bool ProcessPayment(double amount) {
        // Hardcoded database connection!
        PostgresDB db("prod.db.hpn.com:5432");
        
        // Real API call!
        StripeAPI stripe("sk_live_xxx");
        
        auto balance = db.GetBalance(user_id_);
        if (balance < amount) return false;
        
        stripe.Charge(amount);  // Charges REAL money!
        db.UpdateBalance(balance - amount);
        
        return true;
    }
};

💀 PROBLEMS

  1. Cannot test without hitting real database
  2. Cannot test without charging real money
  3. Cannot control test scenarios (what if DB returns error?)
  4. Every test run = real money charged!

Solution: Dependency Injection

cpp
// ✅ TESTABLE CODE — Dependency Injection
class IDatabase {
public:
    virtual ~IDatabase() = default;
    virtual double GetBalance(int user_id) = 0;
    virtual void UpdateBalance(int user_id, double amount) = 0;
};

class IPaymentGateway {
public:
    virtual ~IPaymentGateway() = default;
    virtual bool Charge(double amount) = 0;
};

class PaymentService {
public:
    PaymentService(std::shared_ptr<IDatabase> db,
                   std::shared_ptr<IPaymentGateway> gateway)
        : db_(db), gateway_(gateway) {}
    
    bool ProcessPayment(int user_id, double amount) {
        auto balance = db_->GetBalance(user_id);
        if (balance < amount) return false;
        
        if (!gateway_->Charge(amount)) return false;
        
        db_->UpdateBalance(user_id, balance - amount);
        return true;
    }

private:
    std::shared_ptr<IDatabase> db_;
    std::shared_ptr<IPaymentGateway> gateway_;
};
┌─────────────────────────────────────────────────────────────────────────┐
│                    DEPENDENCY INJECTION VISUALIZATION                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   PRODUCTION:                                                           │
│   ────────────                                                          │
│   ┌─────────────────┐     ┌─────────────────┐                          │
│   │ PaymentService  │────►│  PostgresDB     │──► Real Database          │
│   │                 │────►│  StripeAPI      │──► Real Stripe            │
│   └─────────────────┘     └─────────────────┘                          │
│                                                                         │
│   TESTING:                                                              │
│   ─────────                                                             │
│   ┌─────────────────┐     ┌─────────────────┐                          │
│   │ PaymentService  │────►│  MockDatabase   │──► In-memory fake         │
│   │  (same code!)   │────►│  MockPayment    │──► No real charges        │
│   └─────────────────┘     └─────────────────┘                          │
│                                                                         │
│   → Same PaymentService code, different dependencies!                   │
│   → Trong test: control mọi scenario (success, failure, error)         │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

The Testing Pyramid

┌─────────────────────────────────────────────────────────────────────────┐
│                    THE TESTING PYRAMID                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│                           ┌───────────┐                                 │
│                           │   E2E     │  ← Slow, Expensive, Flaky       │
│                           │  Tests    │     (Selenium, Cypress)         │
│                          ─┴───────────┴─                                │
│                         ╱               ╲                               │
│                        ╱   Integration   ╲  ← Medium speed              │
│                       ╱      Tests        ╲    (DB, API calls)          │
│                      ─────────────────────                              │
│                     ╱                       ╲                           │
│                    ╱      UNIT TESTS         ╲  ← Fast, Cheap, Reliable │
│                   ╱        (GTest/GMock)      ╲    (milliseconds)       │
│                  ───────────────────────────────                        │
│                                                                         │
│   Recommended Ratio:                                                    │
│   • Unit Tests:        70%  (thousands of tests)                        │
│   • Integration Tests: 20%  (hundreds of tests)                         │
│   • E2E Tests:         10%  (tens of tests)                             │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Google Test Setup (CMake)

Installation

bash
# Via Conan
conan install gtest/1.14.0@

# Via Vcpkg
vcpkg install gtest

# Via FetchContent (recommended)
# See CMakeLists.txt below

CMakeLists.txt

cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# ============================================
# OPTION: Enable Testing
# ============================================
option(BUILD_TESTS "Build unit tests" ON)

# ============================================
# Main Library
# ============================================
add_library(mylib
    src/payment_service.cpp
)
target_include_directories(mylib PUBLIC include)

# ============================================
# Google Test via FetchContent
# ============================================
if(BUILD_TESTS)
    include(FetchContent)
    
    FetchContent_Declare(
        googletest
        GIT_REPOSITORY https://github.com/google/googletest.git
        GIT_TAG        v1.14.0
    )
    
    # For Windows: Prevent overriding the parent project's compiler/linker settings
    set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
    
    FetchContent_MakeAvailable(googletest)
    
    enable_testing()
    
    # ============================================
    # Test Executable
    # ============================================
    add_executable(tests
        tests/payment_service_test.cpp
        tests/calculator_test.cpp
    )
    
    target_link_libraries(tests PRIVATE
        mylib
        GTest::gtest
        GTest::gtest_main
        GTest::gmock
    )
    
    include(GoogleTest)
    gtest_discover_tests(tests)
endif()

Directory Structure

project/
├── CMakeLists.txt
├── include/
│   └── payment_service.hpp
├── src/
│   └── payment_service.cpp
└── tests/
    ├── payment_service_test.cpp
    └── calculator_test.cpp

Your First Test

cpp
// tests/calculator_test.cpp
#include <gtest/gtest.h>

int Add(int a, int b) {
    return a + b;
}

// TEST(TestSuiteName, TestName)
TEST(CalculatorTest, AddPositiveNumbers) {
    EXPECT_EQ(Add(2, 3), 5);
}

TEST(CalculatorTest, AddNegativeNumbers) {
    EXPECT_EQ(Add(-2, -3), -5);
}

TEST(CalculatorTest, AddZero) {
    EXPECT_EQ(Add(5, 0), 5);
    EXPECT_EQ(Add(0, 5), 5);
}

Run Tests

bash
# Build
cmake -B build -DBUILD_TESTS=ON
cmake --build build

# Run all tests
cd build && ctest --output-on-failure

# Or run directly for verbose output
./build/tests

Output

[==========] Running 3 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 3 tests from CalculatorTest
[ RUN      ] CalculatorTest.AddPositiveNumbers
[       OK ] CalculatorTest.AddPositiveNumbers (0 ms)
[ RUN      ] CalculatorTest.AddNegativeNumbers
[       OK ] CalculatorTest.AddNegativeNumbers (0 ms)
[ RUN      ] CalculatorTest.AddZero
[       OK ] CalculatorTest.AddZero (0 ms)
[----------] 3 tests from CalculatorTest (0 ms total)

[==========] 3 tests from 1 test suite ran. (0 ms total)
[  PASSED  ] 3 tests.

HPN Tunnel Smoke Test (@[/smoke-test])

💡 SMOKE TEST IN HPN TUNNEL

Trong HPN Tunnel, trước khi chạy full test suite, chúng tôi chạy "Smoke Test" để verify:

  1. Network Interface Ready — có thể bind port
  2. Config File Valid — parse được config
  3. Memory Allocation — có đủ RAM cho operation
  4. Dependencies Available — OpenSSL, zlib loaded

Nếu bất kỳ check nào fail → ABORT ngay, không waste time chạy 2000 tests.

cpp
// smoke_test.cpp
TEST(SmokeTest, NetworkInterfaceAvailable) {
    auto socket = CreateSocket();
    ASSERT_NE(socket, nullptr) << "Cannot create socket";
    
    auto result = socket->Bind(8080);
    ASSERT_TRUE(result.ok()) << "Cannot bind to port 8080";
}

TEST(SmokeTest, ConfigFileValid) {
    auto config = LoadConfig("config.yaml");
    ASSERT_TRUE(config.has_value()) << "Config file not found or invalid";
    
    ASSERT_GT(config->max_connections, 0);
    ASSERT_FALSE(config->server_address.empty());
}

Module Structure

Bài họcNội dung chínhWorkflow
📚 GTest BasicsASSERT vs EXPECT, Fixtures@[/review]
🎭 GMockMocking, Dependency Injection@[/refactor]
🔬 Advanced TestingTEST_P, Edge Cases, Coverage@[/deep-test]
🔄 TDDRed → Green → Refactor@[/review]

Prerequisites

Trước khi học module này, bạn cần:


Bước tiếp theo

📚 GTest Basics → — ASSERT vs EXPECT, Test Fixtures