본문으로 건너뛰기 Google Test for C++: From Setup to TEST, Fixtures, and CI

Google Test for C++: From Setup to TEST, Fixtures, and CI

Google Test  for C++: From Setup to TEST, Fixtures, and CI

Introduction: “I refactored and something else broke”

Table of contents

  1. Setup with CMake
  2. Basic assertions
  3. Test fixtures
  4. Parameterized tests
  5. Death tests
  6. Real-world examples
  7. TDD workflow
  8. Common errors
  9. Debugging tips
  10. Best practices
  11. CI integration
  12. Production patterns

1. Setup with CMake

cmake_minimum_required(VERSION 3.14)
project(MyProject)
include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG release-1.12.1
)
# 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()
add_executable(myapp_test 
    test_main.cpp
    test_calculator.cpp
)
target_link_libraries(myapp_test PRIVATE 
    gtest_main
    myapp_lib  # Your library under test
)
include(GoogleTest)
gtest_discover_tests(myapp_test)

Method 2: vcpkg

vcpkg install gtest
find_package(GTest CONFIG REQUIRED)
add_executable(myapp_test test_main.cpp)
target_link_libraries(myapp_test PRIVATE 
    GTest::gtest_main
    myapp_lib
)

Method 3: Conan

# conanfile.txt
[requires]
gtest/1.12.1
[generators]
CMakeDeps
CMakeToolchain
find_package(GTest REQUIRED)
target_link_libraries(myapp_test PRIVATE GTest::gtest_main)

2. Basic assertions

EXPECT vs ASSERT

#include <gtest/gtest.h>
TEST(CalculatorTest, Addition) {
    // EXPECT: continues after failure
    EXPECT_EQ(2 + 2, 4);
    EXPECT_EQ(3 + 3, 6);  // Still runs even if above fails
    
    // ASSERT: stops test on failure
    int* ptr = new int(42);
    ASSERT_NE(ptr, nullptr);  // Must pass before using ptr
    EXPECT_EQ(*ptr, 42);      // Only runs if ptr is valid
    delete ptr;
}

Common assertions

// Boolean
EXPECT_TRUE(condition);
EXPECT_FALSE(condition);
// Comparison
EXPECT_EQ(a, b);   // ==
EXPECT_NE(a, b);   // !=
EXPECT_LT(a, b);   // <
EXPECT_LE(a, b);   // <=
EXPECT_GT(a, b);   // >
EXPECT_GE(a, b);   // >=
// Strings
EXPECT_STREQ("hello", str);      // C-strings
EXPECT_STRNE("world", str);
std::string s = "test";
EXPECT_EQ("test", s);            // std::string works with EQ
// Floating point
EXPECT_FLOAT_EQ(1.0f, result);
EXPECT_DOUBLE_EQ(1.0, result);
EXPECT_NEAR(1.0, result, 0.001); // Within tolerance
// Exceptions
EXPECT_THROW(func(), std::runtime_error);
EXPECT_NO_THROW(func());
EXPECT_ANY_THROW(func());

Custom messages

EXPECT_EQ(expected, actual) << "Failed with input: " << input;

3. Test fixtures (TEST_F)

Basic fixture

class DatabaseTest : public ::testing::Test {
protected:
    Database* db;
    
    // Runs before each test
    void SetUp() override {
        db = new Database("test.db");
        db->connect();
    }
    
    // Runs after each test
    void TearDown() override {
        db->disconnect();
        delete db;
    }
};
TEST_F(DatabaseTest, InsertRecord) {
    EXPECT_TRUE(db->insert("key", "value"));
    EXPECT_EQ(db->get("key"), "value");
}
TEST_F(DatabaseTest, DeleteRecord) {
    db->insert("key", "value");
    EXPECT_TRUE(db->remove("key"));
    EXPECT_EQ(db->get("key"), "");
}

Suite-level setup (shared across tests)

class ExpensiveResourceTest : public ::testing::Test {
protected:
    static Database* shared_db;
    
    // Runs once before all tests in suite
    static void SetUpTestSuite() {
        shared_db = new Database("shared.db");
        shared_db->connect();
    }
    
    // Runs once after all tests in suite
    static void TearDownTestSuite() {
        shared_db->disconnect();
        delete shared_db;
        shared_db = nullptr;
    }
};
Database* ExpensiveResourceTest::shared_db = nullptr;
TEST_F(ExpensiveResourceTest, Test1) {
    EXPECT_NE(shared_db, nullptr);
}
TEST_F(ExpensiveResourceTest, Test2) {
    EXPECT_NE(shared_db, nullptr);
}

4. Parameterized tests

Basic parameterized test

class SquareTest : public ::testing::TestWithParam<std::pair<int, int>> {};
TEST_P(SquareTest, CheckSquare) {
    auto [input, expected] = GetParam();
    EXPECT_EQ(input * input, expected);
}
INSTANTIATE_TEST_SUITE_P(
    SquareValues,
    SquareTest,
    ::testing::Values(
        std::make_pair(0, 0),
        std::make_pair(1, 1),
        std::make_pair(2, 4),
        std::make_pair(3, 9),
        std::make_pair(-2, 4)
    )
);

Multiple parameters with Combine

class StringTest : public ::testing::TestWithParam<std::tuple<std::string, char, int>> {};
TEST_P(StringTest, CountChar) {
    auto [str, ch, expected] = GetParam();
    int count = std::count(str.begin(), str.end(), ch);
    EXPECT_EQ(count, expected);
}
INSTANTIATE_TEST_SUITE_P(
    StringCases,
    StringTest,
    ::testing::Combine(
        ::testing::Values("hello", "world", "test"),
        ::testing::Values('l', 'o', 't'),
        ::testing::Range(0, 3)
    )
);

Range and ValuesIn

// Range: [0, 10) step 2
INSTANTIATE_TEST_SUITE_P(RangeTest, MyTest, ::testing::Range(0, 10, 2));
// ValuesIn: from container
std::vector<int> inputs = {1, 2, 3, 5, 8, 13};
INSTANTIATE_TEST_SUITE_P(FibTest, MyTest, ::testing::ValuesIn(inputs));

5. Death tests

Basic death test

void DivideByZero(int a, int b) {
    assert(b != 0 && "Division by zero");
    return a / b;
}
TEST(MathDeathTest, DivisionByZero) {
    EXPECT_DEATH(DivideByZero(10, 0), "Division by zero");
}
TEST(MathDeathTest, AbortOnError) {
    ASSERT_DEATH({
        if (condition) {
            std::abort();
        }
    }, ".*");  // Regex for expected stderr
}

Platform-specific death tests

TEST(DeathTest, CrossPlatform) {
#ifdef NDEBUG
    EXPECT_DEATH_IF_SUPPORTED(func(), "error");
#else
    EXPECT_DEATH(func(), "error");
#endif
}

6. Real-world examples

Example 1: String utility class

class StringUtils {
public:
    static std::string trim(const std::string& str) {
        size_t start = str.find_first_not_of(" \t\n\r");
        if (start == std::string::npos) return "";
        size_t end = str.find_last_not_of(" \t\n\r");
        return str.substr(start, end - start + 1);
    }
    
    static std::vector<std::string> split(const std::string& str, char delim) {
        std::vector<std::string> result;
        std::stringstream ss(str);
        std::string item;
        while (std::getline(ss, item, delim)) {
            result.push_back(item);
        }
        return result;
    }
};
TEST(StringUtilsTest, TrimWhitespace) {
    EXPECT_EQ(StringUtils::trim("  hello  "), "hello");
    EXPECT_EQ(StringUtils::trim("\t\ntest\r\n"), "test");
    EXPECT_EQ(StringUtils::trim(""), "");
    EXPECT_EQ(StringUtils::trim("   "), "");
}
TEST(StringUtilsTest, SplitString) {
    auto result = StringUtils::split("a,b,c", ',');
    ASSERT_EQ(result.size(), 3);
    EXPECT_EQ(result[0], "a");
    EXPECT_EQ(result[1], "b");
    EXPECT_EQ(result[2], "c");
}

Example 2: HTTP client with mock

class HttpClient {
public:
    virtual ~HttpClient() = default;
    virtual std::string get(const std::string& url) = 0;
};
class RealHttpClient : public HttpClient {
public:
    std::string get(const std::string& url) override {
        // Actual HTTP implementation
        return "real response";
    }
};
class MockHttpClient : public HttpClient {
public:
    std::string get(const std::string& url) override {
        return "mock response";
    }
};
class ApiService {
    HttpClient* client;
public:
    explicit ApiService(HttpClient* c) : client(c) {}
    
    std::string fetchData(const std::string& endpoint) {
        return client->get("https://api.example.com/" + endpoint);
    }
};
TEST(ApiServiceTest, FetchData) {
    MockHttpClient mock;
    ApiService service(&mock);
    
    std::string result = service.fetchData("users");
    EXPECT_EQ(result, "mock response");
}

Example 3: Thread-safe queue

template<typename T>
class ThreadSafeQueue {
    std::queue<T> queue;
    mutable std::mutex mutex;
public:
    void push(T value) {
        std::lock_guard<std::mutex> lock(mutex);
        queue.push(std::move(value));
    }
    
    bool try_pop(T& value) {
        std::lock_guard<std::mutex> lock(mutex);
        if (queue.empty()) return false;
        value = std::move(queue.front());
        queue.pop();
        return true;
    }
    
    size_t size() const {
        std::lock_guard<std::mutex> lock(mutex);
        return queue.size();
    }
};
TEST(ThreadSafeQueueTest, PushAndPop) {
    ThreadSafeQueue<int> q;
    q.push(1);
    q.push(2);
    
    int value;
    ASSERT_TRUE(q.try_pop(value));
    EXPECT_EQ(value, 1);
    ASSERT_TRUE(q.try_pop(value));
    EXPECT_EQ(value, 2);
    EXPECT_FALSE(q.try_pop(value));
}
TEST(ThreadSafeQueueTest, Concurrent) {
    ThreadSafeQueue<int> q;
    const int N = 1000;
    
    std::thread producer([&]() {
        for (int i = 0; i < N; ++i) {
            q.push(i);
        }
    });
    
    std::thread consumer([&]() {
        int count = 0;
        int value;
        while (count < N) {
            if (q.try_pop(value)) {
                ++count;
            }
        }
    });
    
    producer.join();
    consumer.join();
    
    EXPECT_EQ(q.size(), 0);
}

7. TDD workflow

Red-Green-Refactor cycle

// 1. RED: Write failing test
TEST(CalculatorTest, Multiply) {
    Calculator calc;
    EXPECT_EQ(calc.multiply(2, 3), 6);  // Fails: multiply not implemented
}
// 2. GREEN: Minimal implementation
class Calculator {
public:
    int multiply(int a, int b) {
        return a * b;  // Simplest solution
    }
};
// 3. REFACTOR: Improve without breaking test
class Calculator {
public:
    int multiply(int a, int b) {
        // Add validation, logging, etc.
        if (a == 0 || b == 0) return 0;
        return a * b;
    }
};

Test-first development

// Step 1: Write test for new feature
TEST(UserServiceTest, CreateUser) {
    UserService service;
    User user = service.createUser("john", "john@example.com");
    EXPECT_EQ(user.name, "john");
    EXPECT_EQ(user.email, "john@example.com");
    EXPECT_FALSE(user.id.empty());
}
// Step 2: Implement to pass test
class UserService {
public:
    User createUser(const std::string& name, const std::string& email) {
        User user;
        user.id = generateId();
        user.name = name;
        user.email = email;
        return user;
    }
};

8. Common errors and fixes

Error 1: Undefined reference to InitGoogleTest

undefined reference to `testing::InitGoogleTest(int*, char**)'

Fix: Link gtest_main or provide main

target_link_libraries(myapp_test PRIVATE gtest_main)

Error 2: Multiple definitions of main

multiple definition of `main'

Fix: Don’t define main when using gtest_main

// Remove this if using gtest_main
int main(int argc, char** argv) {
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Error 3: pthread error on Linux

undefined reference to `pthread_create'

Fix: Link Threads

find_package(Threads REQUIRED)
target_link_libraries(myapp_test PRIVATE gtest_main Threads::Threads)

Error 4: Death test fails in Release

Expected: DivideByZero(10, 0) dies
  Actual: it didn't die

Fix: assert() is disabled in Release (NDEBUG)

// Use custom assertion that works in Release
#define MY_ASSERT(cond, msg) \
    if (!(cond)) { \
        std::cerr << msg << std::endl; \
        std::abort(); \
    }

Error 5: Flaky tests in CI

Test passed locally but fails randomly in CI

Fix: Remove nondeterminism

// BAD: depends on timing
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// GOOD: use synchronization
std::condition_variable cv;
std::mutex m;
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, []{ return ready; });

9. Debugging tips

Tip 1: Run specific tests

# Run all tests in suite
./myapp_test --gtest_filter=CalculatorTest.*
# Run specific test
./myapp_test --gtest_filter=CalculatorTest.Addition
# Exclude tests
./myapp_test --gtest_filter=-*Slow*

Tip 2: Repeat tests

# Repeat 100 times to catch flaky tests
./myapp_test --gtest_repeat=100
# Break on first failure
./myapp_test --gtest_repeat=100 --gtest_break_on_failure

Tip 3: Verbose output

# Show all test names
./myapp_test --gtest_list_tests
# Colorized output
./myapp_test --gtest_color=yes

Tip 4: SCOPED_TRACE for loops

TEST(LoopTest, MultipleValues) {
    std::vector<int> inputs = {1, 2, 3, 4, 5};
    for (size_t i = 0; i < inputs.size(); ++i) {
        SCOPED_TRACE("Testing input at index " + std::to_string(i));
        EXPECT_GT(inputs[i], 0);
    }
}

Tip 5: Debug with GDB

# Build with debug symbols
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Run under GDB
gdb --args ./myapp_test --gtest_filter=MyTest.Specific
(gdb) break MyTest_Specific_Test::TestBody
(gdb) run

10. Best practices

1. One assertion per test (when possible)

// BAD: Multiple unrelated assertions
TEST(UserTest, Everything) {
    User user("john");
    EXPECT_EQ(user.getName(), "john");
    EXPECT_TRUE(user.isValid());
    EXPECT_EQ(user.getAge(), 0);
}
// GOOD: Separate tests
TEST(UserTest, GetName) {
    User user("john");
    EXPECT_EQ(user.getName(), "john");
}
TEST(UserTest, IsValid) {
    User user("john");
    EXPECT_TRUE(user.isValid());
}

2. Descriptive test names

// BAD
TEST(UserTest, Test1) { }
// GOOD
TEST(UserTest, GetName_WithValidInput_ReturnsName) { }
TEST(UserTest, IsValid_WithEmptyName_ReturnsFalse) { }

3. AAA pattern (Arrange-Act-Assert)

TEST(CalculatorTest, Add_TwoPositiveNumbers_ReturnsSum) {
    // Arrange
    Calculator calc;
    int a = 2, b = 3;
    
    // Act
    int result = calc.add(a, b);
    
    // Assert
    EXPECT_EQ(result, 5);
}

4. Test edge cases

TEST(StringTest, EdgeCases) {
    EXPECT_EQ(trim(""), "");           // Empty
    EXPECT_EQ(trim(" "), "");          // Only whitespace
    EXPECT_EQ(trim("a"), "a");         // Single char
    EXPECT_EQ(split("", ',').size(), 1); // Empty split
}

5. Avoid test interdependence

// BAD: Tests depend on order
static int counter = 0;
TEST(BadTest, First) { counter = 1; }
TEST(BadTest, Second) { EXPECT_EQ(counter, 1); } // Fragile!
// GOOD: Each test is independent
TEST(GoodTest, First) {
    int counter = 0;
    counter = 1;
    EXPECT_EQ(counter, 1);
}

11. CI integration

GitHub Actions

name: Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Install dependencies
        run: sudo apt-get install -y cmake g++
      
      - name: Build
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Release
          cmake --build build
      
      - name: Run tests
        run: |
          cd build
          ctest --output-on-failure
      
      - name: Generate coverage
        run: |
          cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON
          cmake --build build
          cd build
          ctest
          lcov --capture --directory . --output-file coverage.info
          lcov --remove coverage.info '/usr/*' --output-file coverage.info
          lcov --list coverage.info

CTest integration

enable_testing()
add_executable(myapp_test test_main.cpp)
target_link_libraries(myapp_test PRIVATE gtest_main myapp_lib)
include(GoogleTest)
gtest_discover_tests(myapp_test)
# Run all tests
ctest
# Verbose output
ctest -V
# Run specific test
ctest -R CalculatorTest
# Parallel execution
ctest -j4

12. Production patterns

Pattern 1: Separate test directory

project/
├── CMakeLists.txt
├── src/
│   ├── calculator.h
│   └── calculator.cpp
└── test/
    ├── CMakeLists.txt
    └── test_calculator.cpp

Pattern 2: Test utilities

// test/test_utils.h
namespace test_utils {
    inline std::string readFile(const std::string& path) {
        std::ifstream file(path);
        return std::string((std::istreambuf_iterator<char>(file)),
                          std::istreambuf_iterator<char>());
    }
    
    inline void createTempFile(const std::string& path, const std::string& content) {
        std::ofstream file(path);
        file << content;
    }
}

Pattern 3: Custom matchers

testing::AssertionResult IsEven(int n) {
    if (n % 2 == 0)
        return testing::AssertionSuccess();
    else
        return testing::AssertionFailure() << n << " is odd";
}
TEST(NumberTest, EvenNumbers) {
    EXPECT_TRUE(IsEven(2));
    EXPECT_TRUE(IsEven(4));
}

Summary

  • Setup: FetchContent, vcpkg, or Conan
  • Assertions: EXPECT_* (continue) vs ASSERT_* (stop)
  • Fixtures: TEST_F for setup/teardown
  • Parameterized: TEST_P for data-driven tests
  • Death tests: EXPECT_DEATH for abort/exit verification
  • TDD: Red-Green-Refactor cycle
  • CI: ctest integration with GitHub Actions
  • Best practices: One assertion per test, descriptive names, AAA pattern Next: Google Mock (#18-2)
    Previous: Package managers (#17-2)

Keywords

Google Test, gtest, C++ unit testing, TDD, CTest, test fixtures, parameterized tests


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Complete C++ unit testing guide with Google Test: FetchContent and vcpkg setup, TEST and TEST_F, EXPECT vs ASSERT, param…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, testing, Google Test, gtest, unit-testing, TDD, CTest.