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:
#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:
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:
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:
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<<`
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:
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:
# 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:
# 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:
# 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)
# 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)
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:
# 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:
// 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
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
#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.