Giao diện
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 Test và Google Mock.
Module này chuyển đổi kỹ sư từ "manual console testing" sang "automated CI pipelines" với Google Test và Google 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
- Cannot test without hitting real database
- Cannot test without charging real money
- Cannot control test scenarios (what if DB returns error?)
- 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 belowCMakeLists.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.cppYour 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/testsOutput
[==========] 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:
- Network Interface Ready — có thể bind port
- Config File Valid — parse được config
- Memory Allocation — có đủ RAM cho operation
- 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ọc | Nội dung chính | Workflow |
|---|---|---|
| 📚 GTest Basics | ASSERT vs EXPECT, Fixtures | @[/review] |
| 🎭 GMock | Mocking, Dependency Injection | @[/refactor] |
| 🔬 Advanced Testing | TEST_P, Edge Cases, Coverage | @[/deep-test] |
| 🔄 TDD | Red → Green → Refactor | @[/review] |
Prerequisites
Trước khi học module này, bạn cần:
- ✅ Part 10: Build Systems — CMake setup
- ✅ Part 5: Templates — Để hiểu GMock macros
- ✅ Part 9: Memory Management — Smart pointers