CODE: Debugging Google Test — Tips and Techniques

Debugging failing Google Test cases requires specific techniques for printing diagnostic information, inspecting test state, and isolating failures. This post covers practical debugging approaches including stderr output, test filtering, and attaching debuggers to GTest test runners.

Writing tests with Google Test is straightforward, but debugging when those tests fail — especially in complex scenarios with shared state, templated code, or asynchronous behavior — requires specific techniques. This post covers the essential tools and approaches for diagnosing and fixing failing GTest tests.


The Fundamental Problem with Test Failures

When a GTest assertion fails, it prints the file, line number, expected value, and actual value. But this is not always enough information. Often you need to understand:

  • What state the system was in when the failure occurred
  • What inputs led to the failure
  • What happened in the code path before the failure

The techniques in this post address exactly these needs.


Printing Diagnostic Information with `std::cerr`

The simplest and most immediately useful debugging technique is printing to stderr from inside your test:

cpp
#include <gtest/gtest.h>

TEST(HttpClientTest, SuccessfulGetRequest) {
    HttpClient client("https://api.example.com");
    auto response = client.get("/status");

    // Print diagnostic information before asserting
    std::cerr << "Request URL: " << client.last_url() << std::endl;
    std::cerr << "Response status: " << response.status_code << std::endl;
    std::cerr << "Response body: " << response.body << std::endl;

    EXPECT_EQ(response.status_code, 200);
    EXPECT_FALSE(response.body.empty());
}

GTest does not suppress stderr output, so this information appears in the console output even when the test passes. This is intentional during debugging — you can remove the cerr lines once the test is stable.


Using GTest's Built-in `RecordProperty`

Google Test supports attaching key-value metadata to test results:

cpp
TEST(PerformanceTest, LargeDataSet) {
    auto data = generate_test_data(10000);

    RecordProperty("data_size", 10000);
    RecordProperty("algorithm", "merge_sort");

    auto start = std::chrono::steady_clock::now();
    auto result = process(data);
    auto end = std::chrono::steady_clock::now();

    auto duration_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        end - start).count();
    RecordProperty("duration_ms", duration_ms);

    EXPECT_TRUE(result.is_valid());
}

This metadata is included in XML or JSON test reports, useful for CI integration and test analytics.


Scoped Trace for Context

When a test fails inside a helper function, the stack trace shows the helper but not the calling context. SCOPED_TRACE adds a message to every failure within its scope:

cpp
void CheckValue(int value, int expected) {
    EXPECT_EQ(value, expected);  // Line number here is inside helper
}

TEST(MyTest, MultipleValues) {
    std::vector<int> inputs = {1, 2, 3, 4, 5};
    std::vector<int> expected = {1, 4, 9, 16, 25};  // squares

    for (int i = 0; i < inputs.size(); ++i) {
        SCOPED_TRACE("i = " + std::to_string(i));  // adds context to failures
        CheckValue(square(inputs[i]), expected[i]);
    }
}

If the test fails, the output includes:

text
src/test.cpp:12: Failure
  Trace:
    src/test.cpp:8: i = 2


Printing Complex Objects

For types that have no built-in string representation, you need to define how they are printed for assertion messages.

Using `operator<<`

cpp
struct Point { int x, y; };

// Define stream output for better assertion messages
std::ostream& operator<<(std::ostream& os, const Point& p) {
    return os << "Point{" << p.x << ", " << p.y << "}";
}

TEST(PointTest, Equality) {
    Point p1{3, 4};
    Point p2{3, 5};
    EXPECT_EQ(p1, p2);  // Now prints: Expected: Point{3, 4}, Actual: Point{3, 5}
}

Using `PrintTo`

For types you cannot modify (third-party classes), define PrintTo:

cpp
namespace testing {
void PrintTo(const ThirdPartyType& value, std::ostream* os) {
    *os << "ThirdPartyType{" << value.id() << "}";
}
}


Running and Filtering Tests

When debugging, you typically want to run only the failing test to minimize noise:

bash
# Run a specific test suite and test case
./my_tests --gtest_filter=HttpClientTest.SuccessfulGetRequest

# Run all tests in a suite
./my_tests --gtest_filter=HttpClientTest.*

# Run tests matching a pattern
./my_tests --gtest_filter=*Request*

# Exclude tests matching a pattern
./my_tests --gtest_filter=-*Slow*

# Combine: run suite except slow tests
./my_tests --gtest_filter=HttpClientTest.*-HttpClientTest.*Slow

The filter syntax uses * as a wildcard and : to separate multiple patterns.


Repeating Tests to Find Flaky Failures

Intermittent failures (flaky tests) are common in tests involving timing, randomness, or concurrency:

bash
# Run the test 100 times to find intermittent failures
./my_tests --gtest_repeat=100

# Run until failure
./my_tests --gtest_repeat=-1 --gtest_break_on_failure

# Shuffle test order to find order-dependent failures
./my_tests --gtest_shuffle


Breaking on Test Failure

When running under a debugger, you can have Google Test trigger a debugger breakpoint on failure:

bash
# Break into debugger on first failure (with --gtest_break_on_failure)
gdb ./my_tests
(gdb) run --gtest_break_on_failure --gtest_filter=MyTest.FailingTest

The process will pause at the point of failure, allowing you to inspect the call stack, variables, and memory.


Attaching a Debugger to a Test

With GDB (Linux/macOS)

bash
# Run the test binary directly under GDB
gdb ./my_tests

# Set a breakpoint at a specific test
(gdb) break MyTestSuite_TestName_Test::TestBody
(gdb) run --gtest_filter=MyTestSuite.TestName

# Or break at a specific source line
(gdb) break src/mylib.cpp:42
(gdb) run

With LLDB (macOS)

bash
lldb ./my_tests
(lldb) breakpoint set --name "MyTestSuite_TestName_Test::TestBody"
(lldb) process launch -- --gtest_filter=MyTestSuite.TestName

With IDE Debuggers

Most IDEs (CLion, VS Code with C++ extension, Visual Studio) support running GTest tests directly with the debugger. In CLion, right-click a test and select "Debug". The debugger stops at any breakpoints or assertion failures.


Diagnosing Memory Issues

Google Test works seamlessly with memory analysis tools:

bash
# Valgrind: detect memory errors (Linux)
valgrind --leak-check=full ./my_tests

# AddressSanitizer: fast memory error detection
# Add to CMakeLists.txt:
target_compile_options(my_tests PRIVATE -fsanitize=address -g)
target_link_options(my_tests PRIVATE -fsanitize=address)
./my_tests

# ThreadSanitizer: detect data races
target_compile_options(my_tests PRIVATE -fsanitize=thread -g)

AddressSanitizer is the recommended first line of defense — it catches buffer overflows, use-after-free, and memory leaks with typically 2x runtime overhead.


Disabling Tests Temporarily

When a test is known to be broken and you need to temporarily skip it:

cpp
// Prefix with DISABLED_ to skip without deleting
TEST(MyTest, DISABLED_BrokenTest) {
    // This test will not run, but its existence is reported
}

GTest prints a summary at the end: 1 test DISABLED. This is better than commenting out the test because:

  • The test still compiles (catches syntax errors)
  • The disabled state is visible in test reports
  • It is easy to re-enable

Common Debugging Checklist

text
GTest Debugging Checklist:
+-------------------------------------------+
| 1. Read the failure message carefully      |
|    - What was expected vs. actual?         |
|    - Which file and line?                  |
+-------------------------------------------+
| 2. Add cerr prints to trace values         |
|    - Print inputs, intermediate values     |
|    - Print before and after mutations      |
+-------------------------------------------+
| 3. Use --gtest_filter to isolate           |
|    - Run only the failing test             |
|    - Reduce noise from passing tests       |
+-------------------------------------------+
| 4. Use SCOPED_TRACE in loops/helpers       |
|    - Add context to nested failures        |
+-------------------------------------------+
| 5. Run under sanitizers                    |
|    - ASan for memory errors               |
|    - TSan for data races                   |
+-------------------------------------------+
| 6. Use --gtest_repeat for flaky tests      |
|    - Reproduce intermittent failures       |
+-------------------------------------------+
| 7. Attach debugger for complex failures    |
|    - Set breakpoints at assertion points   |
+-------------------------------------------+


Complete Debugging Example

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

class DataProcessor {
public:
    std::vector<int> process(const std::vector<int>& input) {
        std::vector<int> result;
        for (int x : input) {
            result.push_back(x * x);
        }
        return result;
    }
};

class DataProcessorTest : public ::testing::Test {
protected:
    void SetUp() override {
        processor = std::make_unique<DataProcessor>();
    }

    std::unique_ptr<DataProcessor> processor;
};

TEST_F(DataProcessorTest, SquaresPositiveNumbers) {
    std::vector<int> input = {1, 2, 3, 4, 5};
    auto result = processor->process(input);

    // Diagnostic output for debugging
    std::cerr << "Input size: " << input.size() << std::endl;
    std::cerr << "Result size: " << result.size() << std::endl;

    ASSERT_EQ(result.size(), input.size());

    for (size_t i = 0; i < input.size(); ++i) {
        SCOPED_TRACE("index " + std::to_string(i));
        EXPECT_EQ(result[i], input[i] * input[i]);
    }
}


Conclusion

Effective GTest debugging combines simple cerr output for quick diagnosis, SCOPED_TRACE for context in nested failures, test filtering to isolate issues, and memory sanitizers for hard-to-reproduce bugs. These techniques, combined with a disciplined approach to test isolation and reproducibility, make even complex C++ bugs tractable.

The goal is not just to make tests pass — it is to understand why they fail, fix the root cause, and ensure the failure cannot recur. Google Test provides all the tools you need to achieve this systematically.