Master C++ Testing: GoogleTest vs Catch2 vs doctest Guide
Compare GoogleTest, Catch2, and doctest for C++ unit testing.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Basic C++ knowledge (classes, inheritance, templates).
- ✓Familiarity with CMake or build systems.
- ✓Understanding of unit testing concepts.
- GoogleTest: Mature, feature-rich, best for large projects with mocking needs.
- Catch2: Header-only, expressive BDD-style, ideal for quick tests.
- doctest: Fastest compile times, minimal overhead, great for embedded or header-only libraries.
Think of testing frameworks like different toolkits for checking your code's health. GoogleTest is a full hospital lab with advanced equipment (mocking, death tests). Catch2 is a portable first-aid kit you can carry anywhere. doctest is a tiny Swiss Army knife that fits in your pocket and works instantly.
In modern C++ development, unit testing is not optional—it's a safety net that catches regressions, documents behavior, and enables fearless refactoring. Three frameworks dominate the landscape: GoogleTest, Catch2, and doctest. Each offers unique trade-offs in compile time, feature set, and syntax. This tutorial provides a hands-on comparison, guiding you through setup, writing tests, mocking, and debugging real-world issues. By the end, you'll know which framework fits your project's needs and how to avoid common pitfalls.
Setting Up GoogleTest
GoogleTest (gtest) is a comprehensive framework developed by Google. It supports test discovery, assertions, death tests, and mocking via GoogleMock. To set up, clone the repository and build with CMake:
git clone https://github.com/google/googletest.git
cd googletest
mkdir build && cd build
cmake .. -DCMAKE_CXX_STANDARD=17
make
sudo make install
Alternatively, use FetchContent in CMake to integrate directly:
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
FetchContent_MakeAvailable(googletest)
Link your test executable with gtest and gtest_main.
Writing Tests with Catch2
Catch2 is a header-only testing framework with a natural BDD-style syntax. It supports sections, generators, and matchers. To use, download the single header or use vcpkg:
vcpkg install catch2
Include the header and define CATCH_CONFIG_MAIN in exactly one source file:
#define CATCH_CONFIG_MAIN
#include <catch2/catch.hpp>
Write tests using TEST_CASE and SECTION macros:
TEST_CASE("Addition works", "[math]") {
REQUIRE(add(2, 3) == 5);
SECTION("with zero") {
REQUIRE(add(0, 5) == 5);
}
SECTION("with negatives") {
REQUIRE(add(-1, -1) == -2);
}
}
Catch2's SECTION allows hierarchical test organization without separate test cases.
Lightweight Testing with doctest
doctest is the fastest C++ testing framework, designed for compile-time efficiency. It is header-only and can be used in both source and header files. Setup is minimal:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>
Write tests with TEST_CASE and CHECK/REQUIRE macros:
TEST_CASE("string concatenation") {
std::string a = "Hello ";
std::string b = "World";
CHECK((a + b) == "Hello World");
}
doctest supports subcases (similar to Catch2 sections) and has a minimal footprint, making it ideal for embedded systems or libraries where compile time matters.
Mocking with GoogleMock
GoogleMock (gmock) integrates with GoogleTest to create mock objects. Define a mock class by inheriting from a base interface and using MOCK_METHOD macros:
class Database {
public:
virtual ~Database() = default;
virtual bool save(const std::string& data) = 0;
};
class MockDatabase : public Database {
public:
MOCK_METHOD(bool, save, (const std::string&), (override));
};
In tests, set expectations:
TEST(DatabaseTest, SaveCalled) {
MockDatabase mock;
EXPECT_CALL(mock, save("test")).Times(1).WillOnce(::testing::Return(true));
// code that uses mock
}
Use NiceMock to suppress warnings about unexpected calls, or StrictMock to fail on any unexpected call.
Parameterized Tests and Data-Driven Testing
All three frameworks support parameterized tests to run the same test logic with different inputs.
GoogleTest: Use TEST_P with INSTANTIATE_TEST_SUITE_P.
class ParamTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};
TEST_P(ParamTest, Sum) {
auto [a, b, expected] = GetParam();
EXPECT_EQ(add(a, b), expected);
}
INSTANTIATE_TEST_SUITE_P(Default, ParamTest,
::testing::Values(
std::make_tuple(1, 1, 2),
std::make_tuple(2, 3, 5)
));
Catch2: Use GENERATE inside a TEST_CASE.
TEST_CASE("parameterized sum") {
auto [a, b, expected] = GENERATE(table<int, int, int>({
{1, 1, 2},
{2, 3, 5}
}));
REQUIRE(add(a, b) == expected);
}
doctest: Use TEST_CASE with SUBCASE or manual loops.
TEST_CASE("parameterized sum") {
int a, b, expected;
SUBCASE("1+1=2") { a=1; b=1; expected=2; }
SUBCASE("2+3=5") { a=2; b=3; expected=5; }
CHECK(add(a, b) == expected);
}Best Practices and Common Pitfalls
Best Practices: - Write tests before code (TDD) to ensure testability. - Use descriptive test names that explain the scenario. - Keep tests independent; avoid shared mutable state. - Use fixtures for common setup. - Run tests in CI with sanitizers (AddressSanitizer, UndefinedBehaviorSanitizer).
Common Pitfalls: - Testing implementation details: Test behavior, not internal state. - Flaky tests: Avoid randomness, timeouts, and external dependencies. - Ignoring test failures: Treat failures as critical bugs. - Over-mocking: Mock only external dependencies; use real objects for value types.
Example of a well-structured test:
``cpp TEST_F(MyFixture, ReturnsCorrectValueWhenInputIsValid) { // Arrange auto input = createValidInput(); // Act auto result = objectUnderTest.process(input); // Assert EXPECT_EQ(result, expectedValue); } ``
The Silent Regression: How a Missing Test Cost $50k
- Always test edge cases (empty, null, boundary values).
- Run tests automatically on every commit.
- Use code coverage tools to identify untested paths.
- Mock external dependencies to isolate unit tests.
- Write tests before refactoring to catch regressions early.
g++ -fsanitize=address -g test.cpp -lgtest -lpthread./a.out| File | Command / Code | Purpose |
|---|---|---|
| test_example.cpp | int add(int a, int b) { return a + b; } | Setting Up GoogleTest |
| catch2_example.cpp | int multiply(int a, int b) { return a * b; } | Writing Tests with Catch2 |
| doctest_example.cpp | int factorial(int n) { | Lightweight Testing with doctest |
| gmock_example.cpp | class Logger { | Mocking with GoogleMock |
| param_test.cpp | int add(int a, int b) { return a + b; } | Parameterized Tests and Data-Driven Testing |
| best_practices.cpp | class Calculator { | Best Practices and Common Pitfalls |
Key takeaways
Interview Questions on This Topic
Explain the difference between unit tests and integration tests. How do you decide what to test?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C++ Advanced. Mark it forged?
3 min read · try the examples if you haven't