Skip to content

🔬 Advanced Testing Deep Test

Beyond basics: Parameterized tests, Boundary Value Analysis (@[/deep-test]), Flaky test prevention (@[/reliability]), và Code Coverage metrics (@[/review]).

Parameterized Tests (TEST_P)

Vấn đề: Code Duplication

cpp
// ❌ Repeating same logic with different inputs
TEST(CalculatorTest, Add_2_3) { EXPECT_EQ(Add(2, 3), 5); }
TEST(CalculatorTest, Add_0_0) { EXPECT_EQ(Add(0, 0), 0); }
TEST(CalculatorTest, Add_Neg2_Neg3) { EXPECT_EQ(Add(-2, -3), -5); }
TEST(CalculatorTest, Add_100_200) { EXPECT_EQ(Add(100, 200), 300); }
// ... 50 more tests ...

Solution: TEST_P

cpp
#include <gtest/gtest.h>
#include <tuple>

// Step 1: Define test parameters
struct AddTestParams {
    int a;
    int b;
    int expected;
    std::string description;
};

// Step 2: Create parameterized test fixture
class AddTest : public ::testing::TestWithParam<AddTestParams> {};

// Step 3: Write single test that uses parameters
TEST_P(AddTest, ReturnsCorrectSum) {
    auto params = GetParam();
    EXPECT_EQ(Add(params.a, params.b), params.expected) 
        << "Failed for: " << params.description;
}

// Step 4: Instantiate with test values
INSTANTIATE_TEST_SUITE_P(
    CalculatorTests,
    AddTest,
    ::testing::Values(
        AddTestParams{2, 3, 5, "positive + positive"},
        AddTestParams{0, 0, 0, "zero + zero"},
        AddTestParams{-2, -3, -5, "negative + negative"},
        AddTestParams{-5, 5, 0, "negative + positive = zero"},
        AddTestParams{INT_MAX, 0, INT_MAX, "max int"},
        AddTestParams{0, INT_MIN, INT_MIN, "min int"}
    )
);

Output

[ RUN      ] CalculatorTests/AddTest.ReturnsCorrectSum/0
[       OK ] CalculatorTests/AddTest.ReturnsCorrectSum/0 (0 ms)
[ RUN      ] CalculatorTests/AddTest.ReturnsCorrectSum/1
[       OK ] CalculatorTests/AddTest.ReturnsCorrectSum/1 (0 ms)
...

Boundary Value Analysis (@[/deep-test])

Banking Transaction Example

┌─────────────────────────────────────────────────────────────────────────┐
│                    BOUNDARY VALUE ANALYSIS                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Function: Transfer(amount)                                            │
│   Valid range: 0.01 to 1,000,000.00                                     │
│                                                                         │
│   Test values (BVA):                                                    │
│   ───────────────────                                                   │
│   • INVALID: -1.00, 0.00                                                │
│   • BOUNDARY: 0.01 (min valid)                                          │
│   • NORMAL: 500.00                                                      │
│   • BOUNDARY: 1,000,000.00 (max valid)                                  │
│   • INVALID: 1,000,000.01                                               │
│                                                                         │
│   Edge cases:                                                           │
│   ────────────                                                          │
│   • NaN, Infinity (floating point)                                      │
│   • Very small: 0.001 (precision)                                       │
│   • Source = Destination account                                        │
│   • Concurrent transfers (race condition)                               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Implementation

cpp
struct TransferTestParams {
    double amount;
    bool should_succeed;
    std::string reason;
};

class TransferBoundaryTest 
    : public ::testing::TestWithParam<TransferTestParams> {
protected:
    void SetUp() override {
        mock_db_ = std::make_shared<MockDatabase>();
        service_ = std::make_unique<TransferService>(mock_db_);
        
        // Assume source has $10,000 balance
        ON_CALL(*mock_db_, GetBalance(source_account_))
            .WillByDefault(Return(10000.00));
    }
    
    std::shared_ptr<MockDatabase> mock_db_;
    std::unique_ptr<TransferService> service_;
    const int source_account_ = 123;
    const int dest_account_ = 456;
};

TEST_P(TransferBoundaryTest, ValidatesAmount) {
    auto params = GetParam();
    
    if (params.should_succeed) {
        EXPECT_CALL(*mock_db_, Transfer(_, _, params.amount))
            .WillOnce(Return(true));
    }
    
    auto result = service_->Transfer(
        source_account_, dest_account_, params.amount);
    
    EXPECT_EQ(result.ok(), params.should_succeed) 
        << "Failed: " << params.reason;
}

INSTANTIATE_TEST_SUITE_P(
    BoundaryValues,
    TransferBoundaryTest,
    ::testing::Values(
        // Invalid - below minimum
        TransferTestParams{-1.00, false, "Negative amount"},
        TransferTestParams{0.00, false, "Zero amount"},
        TransferTestParams{0.001, false, "Below precision"},
        
        // Valid boundaries
        TransferTestParams{0.01, true, "Minimum valid"},
        TransferTestParams{500.00, true, "Normal amount"},
        TransferTestParams{1000000.00, true, "Maximum valid"},
        
        // Invalid - above maximum
        TransferTestParams{1000000.01, false, "Above maximum"},
        TransferTestParams{1e15, false, "Extremely large"},
        
        // Special floating point
        TransferTestParams{std::numeric_limits<double>::infinity(), 
                           false, "Infinity"},
        TransferTestParams{std::numeric_limits<double>::quiet_NaN(), 
                           false, "NaN"}
    )
);

Flaky Tests — The CI/CD Enemy (@[/reliability])

What is a Flaky Test?

┌─────────────────────────────────────────────────────────────────────────┐
│                    FLAKY TEST VISUALIZATION                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Same code, same test:                                                 │
│                                                                         │
│   Run 1: ✅ PASS                                                         │
│   Run 2: ❌ FAIL                                                         │
│   Run 3: ✅ PASS                                                         │
│   Run 4: ✅ PASS                                                         │
│   Run 5: ❌ FAIL                                                         │
│                                                                         │
│   → Developers lose trust in test suite                                 │
│   → CI pipeline always shows "red" (noise)                              │
│   → Real bugs hidden among flaky failures                               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Common Causes & Solutions

1. Time-dependent Tests

cpp
// ❌ FLAKY: Depends on system time
TEST(SessionTest, ExpiresAfter1Hour) {
    Session session;
    session.Create();
    
    std::this_thread::sleep_for(std::chrono::hours(1));  // SLOW!
    
    EXPECT_TRUE(session.IsExpired());
}

// ✅ FIXED: Inject time provider
class ITimeProvider {
public:
    virtual ~ITimeProvider() = default;
    virtual std::time_t Now() = 0;
};

class MockTimeProvider : public ITimeProvider {
public:
    MOCK_METHOD(std::time_t, Now, (), (override));
};

TEST(SessionTest, ExpiresAfter1Hour) {
    auto mock_time = std::make_shared<MockTimeProvider>();
    Session session(mock_time);
    
    EXPECT_CALL(*mock_time, Now())
        .WillOnce(Return(1000))         // Creation time
        .WillOnce(Return(1000 + 3601)); // 1 hour + 1 second later
    
    session.Create();
    EXPECT_TRUE(session.IsExpired());  // Instant, no sleep!
}

2. Order-dependent Tests

cpp
// ❌ FLAKY: Test depends on global state from previous test
int global_counter = 0;

TEST(CounterTest, Increment) {
    global_counter++;
    EXPECT_EQ(global_counter, 1);  // Fails if another test ran first!
}

// ✅ FIXED: Reset state in SetUp
class CounterTest : public ::testing::Test {
protected:
    void SetUp() override {
        global_counter = 0;  // Always start fresh
    }
};

3. Race Conditions

cpp
// ❌ FLAKY: Race condition
TEST(AsyncTest, TaskCompletes) {
    std::atomic<bool> done{false};
    
    std::thread worker([&]() {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        done = true;
    });
    
    worker.detach();
    
    std::this_thread::sleep_for(std::chrono::milliseconds(15));
    EXPECT_TRUE(done);  // May fail on slow machines!
}

// ✅ FIXED: Use synchronization primitives
TEST(AsyncTest, TaskCompletes) {
    std::promise<void> promise;
    auto future = promise.get_future();
    
    std::thread worker([&]() {
        // Do work
        promise.set_value();  // Signal completion
    });
    
    // Wait with timeout
    auto status = future.wait_for(std::chrono::seconds(5));
    ASSERT_EQ(status, std::future_status::ready);
    
    worker.join();
}

4. Network Dependencies

cpp
// ❌ FLAKY: Depends on network/external service
TEST(ApiTest, FetchesData) {
    HttpClient client;
    auto response = client.Get("https://api.example.com/data");
    EXPECT_EQ(response.status, 200);  // Network down = fail!
}

// ✅ FIXED: Mock external services
TEST(ApiTest, FetchesData) {
    auto mock_http = std::make_shared<MockHttpClient>();
    
    EXPECT_CALL(*mock_http, Get("https://api.example.com/data"))
        .WillOnce(Return(HttpResponse{200, R"({"data": "test"})"}));
    
    ApiService service(mock_http);
    auto result = service.FetchData();
    
    EXPECT_TRUE(result.ok());
}

Detecting Flaky Tests

bash
# Run tests multiple times
./tests --gtest_repeat=100 --gtest_shuffle

# If ANY run fails, you have a flaky test

Code Coverage (@[/review])

Types of Coverage

┌─────────────────────────────────────────────────────────────────────────┐
│                    CODE COVERAGE TYPES                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   LINE COVERAGE:                                                        │
│   ──────────────                                                        │
│   Was this line executed? (Simplest metric)                             │
│                                                                         │
│   if (x > 0) {          ← Line 1: executed                              │
│       DoSomething();    ← Line 2: executed                              │
│   } else {              ← Line 3: NOT executed                          │
│       DoOther();        ← Line 4: NOT executed                          │
│   }                                                                     │
│   Coverage: 2/4 = 50%                                                   │
│                                                                         │
│   BRANCH COVERAGE:                                                      │
│   ─────────────────                                                     │
│   Were both if and else branches taken?                                 │
│                                                                         │
│   if (x > 0)  ← TRUE branch: tested, FALSE branch: NOT tested          │
│   Coverage: 1/2 = 50%                                                   │
│                                                                         │
│   ⚠️ WARNING: 100% line coverage ≠ 100% branch coverage                │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Setting Up Coverage with CMake

cmake
# CMakeLists.txt
option(ENABLE_COVERAGE "Enable code coverage" OFF)

if(ENABLE_COVERAGE AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
    target_compile_options(tests PRIVATE --coverage -O0 -g)
    target_link_options(tests PRIVATE --coverage)
endif()

Generating Reports

bash
# Build with coverage
cmake -B build -DENABLE_COVERAGE=ON
cmake --build build

# Run tests
cd build && ./tests

# Generate report with lcov
lcov --capture --directory . --output-file coverage.info
lcov --remove coverage.info '/usr/*' --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

# Open report
open coverage_report/index.html

Coverage Guidelines

┌─────────────────────────────────────────────────────────────────────────┐
│                    COVERAGE GUIDELINES                                   │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   Target Metrics (HPN Standard):                                        │
│   ────────────────────────────────                                      │
│   • Overall: ≥ 80% line coverage                                        │
│   • Critical paths: 100% branch coverage                                │
│   • New code: 90%+ coverage required                                    │
│                                                                         │
│   ⚠️ COVERAGE PITFALLS:                                                 │
│   ─────────────────────                                                 │
│   • 100% coverage ≠ 0 bugs (can still have logic errors)                │
│   • Don't write useless tests just to hit numbers                       │
│   • Uncovered code is NOT tested, but covered ≠ correctly tested        │
│                                                                         │
│   What to focus on:                                                     │
│   ───────────────────                                                   │
│   • Error handling paths                                                │
│   • Boundary conditions                                                 │
│   • Business logic (payment, auth)                                      │
│   • Not: Getters/setters, simple constructors                           │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Death Tests (Testing Crashes)

cpp
// Testing that code correctly terminates on error
TEST(AssertTest, TerminatesOnInvalidInput) {
    // GTest spawns a subprocess to capture crash
    EXPECT_DEATH(
        ProcessData(nullptr),  // Should crash/abort
        ".*null pointer.*"     // Expected error message regex
    );
}

TEST(AssertTest, ThrowsOrDies) {
    EXPECT_EXIT(
        CriticalFailure(),
        ::testing::ExitedWithCode(1),  // Exit code 1
        "Critical error"               // stderr contains this
    );
}

Bước tiếp theo

🔄 TDD → — Test-Driven Development: Red → Green → Refactor