Giao diện
🔬 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 testCode 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.htmlCoverage 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
);
}