Skip to content

📚 GTest Basics Foundation

Master the fundamentals: ASSERT vs EXPECT, Test Fixtures, và tổ chức test suite như một QA Engineer chuyên nghiệp.

ASSERT vs EXPECT — The Critical Difference

┌─────────────────────────────────────────────────────────────────────────┐
│                    ASSERT vs EXPECT                                      │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ASSERT_*:  Fatal failure                                              │
│   ──────────────────────                                                │
│   • Test STOPS immediately on failure                                   │
│   • Use when continuing makes no sense                                  │
│   • Example: ASSERT_NE(ptr, nullptr) — can't deref null                 │
│                                                                         │
│   EXPECT_*:  Non-fatal failure                                          │
│   ──────────────────────────                                            │
│   • Test CONTINUES after failure                                        │
│   • Collects all failures in one run                                    │
│   • Use for independent checks                                          │
│                                                                         │
│   Rule of Thumb:                                                        │
│   • Use ASSERT for preconditions                                        │
│   • Use EXPECT for the actual assertions                                │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Example

cpp
TEST(PointerTest, ValidPointer) {
    auto* ptr = GetUserData(user_id);
    
    // ASSERT: Nếu null thì không thể tiếp tục
    ASSERT_NE(ptr, nullptr) << "User not found";
    
    // EXPECT: Những check sau đây độc lập
    EXPECT_EQ(ptr->name, "John");
    EXPECT_EQ(ptr->age, 30);
    EXPECT_TRUE(ptr->is_active);
    // Nếu name sai, vẫn check age và is_active
}

Assertion Macros Reference

Boolean Assertions

cpp
ASSERT_TRUE(condition);   // condition == true
ASSERT_FALSE(condition);  // condition == false

EXPECT_TRUE(IsValid(x));
EXPECT_FALSE(IsEmpty(list));

Comparison Assertions

cpp
// Equality
EXPECT_EQ(val1, val2);   // val1 == val2
EXPECT_NE(val1, val2);   // val1 != val2

// Relational
EXPECT_LT(val1, val2);   // val1 < val2
EXPECT_LE(val1, val2);   // val1 <= val2
EXPECT_GT(val1, val2);   // val1 > val2
EXPECT_GE(val1, val2);   // val1 >= val2

String Assertions

cpp
// C-string comparison
EXPECT_STREQ(str1, str2);      // strcmp(str1, str2) == 0
EXPECT_STRNE(str1, str2);      // strcmp(str1, str2) != 0
EXPECT_STRCASEEQ(str1, str2);  // Case-insensitive equal
EXPECT_STRCASENE(str1, str2);  // Case-insensitive not equal

// std::string uses EXPECT_EQ directly
std::string s1 = "hello";
std::string s2 = "hello";
EXPECT_EQ(s1, s2);  // Works!

Floating-Point Assertions

cpp
// ❌ NEVER use EXPECT_EQ for floats!
EXPECT_EQ(0.1 + 0.2, 0.3);  // FAILS! (floating point error)

// ✅ Use EXPECT_FLOAT_EQ / EXPECT_DOUBLE_EQ
EXPECT_FLOAT_EQ(0.1f + 0.2f, 0.3f);   // 4 ULP tolerance
EXPECT_DOUBLE_EQ(0.1 + 0.2, 0.3);     // 4 ULP tolerance

// ✅ Or use EXPECT_NEAR for custom tolerance
EXPECT_NEAR(calculated, expected, 1e-6);

Exception Assertions

cpp
// Expect specific exception type
EXPECT_THROW(ThrowingFunction(), std::runtime_error);

// Expect any exception
EXPECT_ANY_THROW(ThrowingFunction());

// Expect no exception
EXPECT_NO_THROW(SafeFunction());

Custom Failure Messages

cpp
TEST(UserTest, ValidAge) {
    User user = GetUser(123);
    
    EXPECT_GE(user.age, 0) 
        << "Age cannot be negative. Got: " << user.age;
    
    EXPECT_LE(user.age, 150) 
        << "Age seems unrealistic. Got: " << user.age
        << " for user: " << user.name;
}

Output on failure:

[ FAILURE ] UserTest.ValidAge
Expected: user.age >= 0
  Actual: -5
Age cannot be negative. Got: -5

Test Fixtures — SetUp & TearDown

Vấn đề: Duplicate Setup Code

cpp
// ❌ Code duplication in every test
TEST(DatabaseTest, InsertUser) {
    auto db = std::make_unique<Database>();
    db->Connect("localhost", 5432);
    db->Migrate();
    
    // ... actual test ...
    
    db->Disconnect();
}

TEST(DatabaseTest, DeleteUser) {
    auto db = std::make_unique<Database>();  // Duplicate!
    db->Connect("localhost", 5432);           // Duplicate!
    db->Migrate();                            // Duplicate!
    
    // ... actual test ...
    
    db->Disconnect();                         // Duplicate!
}

Solution: Test Fixture

cpp
// ✅ Using Test Fixture
class DatabaseTest : public ::testing::Test {
protected:
    // Runs BEFORE each test
    void SetUp() override {
        db_ = std::make_unique<Database>();
        db_->Connect("localhost", 5432);
        db_->Migrate();
    }
    
    // Runs AFTER each test
    void TearDown() override {
        db_->Disconnect();
    }
    
    std::unique_ptr<Database> db_;
};

// Use TEST_F instead of TEST
TEST_F(DatabaseTest, InsertUser) {
    // db_ is already set up!
    auto result = db_->Insert(User{"John", 30});
    EXPECT_TRUE(result.ok());
}

TEST_F(DatabaseTest, DeleteUser) {
    // Fresh db_ for each test!
    db_->Insert(User{"Jane", 25});
    auto result = db_->Delete("Jane");
    EXPECT_TRUE(result.ok());
}
┌─────────────────────────────────────────────────────────────────────────┐
│                    TEST FIXTURE LIFECYCLE                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   For each TEST_F:                                                      │
│   ─────────────────                                                     │
│                                                                         │
│   1. Create new DatabaseTest object                                     │
│   2. Call SetUp()                                                       │
│   3. Run test body                                                      │
│   4. Call TearDown()                                                    │
│   5. Destroy DatabaseTest object                                        │
│                                                                         │
│   → Each test gets a FRESH fixture instance                             │
│   → Tests are completely ISOLATED                                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Static SetUpTestSuite

Khi setup quá tốn kém (DB connection expensive):

cpp
class ExpensiveTest : public ::testing::Test {
protected:
    // Once for entire test suite
    static void SetUpTestSuite() {
        shared_db_ = CreateExpensiveConnection();
    }
    
    static void TearDownTestSuite() {
        shared_db_.reset();
    }
    
    // Per-test setup (lightweight)
    void SetUp() override {
        shared_db_->BeginTransaction();
    }
    
    void TearDown() override {
        shared_db_->RollbackTransaction();  // Clean state
    }
    
    static std::shared_ptr<Database> shared_db_;
};

std::shared_ptr<Database> ExpensiveTest::shared_db_ = nullptr;

Test Organization

Naming Convention

cpp
// Format: TEST(ClassName_MethodName, WhenCondition_ExpectedBehavior)

TEST(Calculator_Add, WhenBothPositive_ReturnsSum) { }
TEST(Calculator_Add, WhenOneNegative_ReturnsCorrectResult) { }
TEST(Calculator_Divide, WhenDivisorZero_ThrowsException) { }

// Or simpler format for small projects
TEST(CalculatorTest, AddPositiveNumbers) { }
TEST(CalculatorTest, DivideByZeroThrows) { }

File Organization

tests/
├── unit/
│   ├── calculator_test.cpp
│   ├── user_service_test.cpp
│   └── payment_service_test.cpp
├── integration/
│   ├── database_integration_test.cpp
│   └── api_integration_test.cpp
└── fixtures/
    └── test_data.hpp

Disabling Tests

cpp
// Temporarily disable a test
TEST(DISABLED_SlowTest, TakesTooLong) {
    // Won't run, but shows in output as disabled
}

// Disable entire suite
class DISABLED_BrokenTests : public ::testing::Test { };
TEST_F(DISABLED_BrokenTests, WillNotRun) { }

Test Filters (Command Line)

bash
# Run only specific tests
./tests --gtest_filter=CalculatorTest.*

# Run tests matching pattern
./tests --gtest_filter=*Add*

# Exclude tests
./tests --gtest_filter=-SlowTest.*

# Combine
./tests --gtest_filter=Fast*:-*Slow*

# List all tests without running
./tests --gtest_list_tests

# Repeat tests (for flakiness detection)
./tests --gtest_repeat=10

# Shuffle order (detect order dependencies)
./tests --gtest_shuffle

Best Practices

┌─────────────────────────────────────────────────────────────────────────┐
│                    GTEST BEST PRACTICES                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ✅ DO                                                                  │
│   ─────                                                                 │
│   • One logical assertion per test (keeps failures clear)               │
│   • Use descriptive test names                                          │
│   • Use fixtures for shared setup                                       │
│   • Add failure messages to assertions                                  │
│   • Test edge cases and error conditions                                │
│                                                                         │
│   ❌ DON'T                                                               │
│   ───────                                                               │
│   • Don't use ASSERT when EXPECT works                                  │
│   • Don't test multiple units in one test                               │
│   • Don't depend on test execution order                                │
│   • Don't leave DISABLED tests forever                                  │
│   • Don't use sleep() in tests (flaky!)                                 │
│                                                                         │
│   💡 TIPS                                                                │
│   ────────                                                              │
│   • Run with --gtest_shuffle to catch order dependencies                │
│   • Run with --gtest_repeat=N to catch flaky tests                      │
│   • Use SCOPED_TRACE for loops                                          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

SCOPED_TRACE for Loops

cpp
TEST(ArrayTest, AllPositive) {
    std::vector<int> values = {1, 2, -3, 4};
    
    for (size_t i = 0; i < values.size(); ++i) {
        SCOPED_TRACE("Index: " + std::to_string(i));
        EXPECT_GT(values[i], 0);
    }
}

// Output shows WHICH iteration failed:
// [ FAILURE ] ArrayTest.AllPositive
// Index: 2
// Expected: values[i] > 0
//   Actual: -3 <= 0

Bước tiếp theo

Đã học GTest basics, giờ học Mocking:

🎭 GMock → — Mocking với Dependency Injection, EXPECT_CALL