C++ Unit Testing with Google Test and Catch2 tutorial
|

C++ Unit Testing: Google Test & Catch2 Complete Guide 2026

Why Test Your Code

Writing tests feels slow when you could be writing features. But untested code is a liability — every change might break something, and you won’t know until a user finds it. Tests give you confidence to refactor, add features, and fix bugs without fear. In C++ especially, where memory errors and undefined behavior lurk silently, automated tests catch problems that manual testing misses.

The C++ ecosystem has two dominant test frameworks: Google Test (gtest) — the industry standard used by Google, Chromium, LLVM, and countless companies — and Catch2 — a lighter, header-friendly alternative popular in open-source projects. This lesson covers both.

Google Test Setup

# CMakeLists.txt — Add Google Test with FetchContent
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

include(FetchContent)
FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

# Your library
add_library(mathlib src/math.cpp)
target_include_directories(mathlib PUBLIC include)

# Test executable
enable_testing()
add_executable(tests tests/test_math.cpp)
target_link_libraries(tests PRIVATE mathlib GTest::gtest_main)

include(GoogleTest)
gtest_discover_tests(tests)

Writing Your First Test

// include/math.hpp
#pragma once
int add(int a, int b);
int factorial(int n);
bool is_prime(int n);

// src/math.cpp
#include "math.hpp"
int add(int a, int b) { return a + b; }
int factorial(int n) { return n <= 1 ? 1 : n * factorial(n - 1); }
bool is_prime(int n) {
    if (n < 2) return false;
    for (int i = 2; i * i <= n; ++i)
        if (n % i == 0) return false;
    return true;
}

// tests/test_math.cpp
#include <gtest/gtest.h>
#include "math.hpp"

TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
    EXPECT_EQ(add(0, 0), 0);
    EXPECT_EQ(add(100, 200), 300);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(add(-1, -1), -2);
    EXPECT_EQ(add(-5, 5), 0);
}

TEST(FactorialTest, BasicValues) {
    EXPECT_EQ(factorial(0), 1);
    EXPECT_EQ(factorial(1), 1);
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(10), 3628800);
}

TEST(PrimeTest, KnownPrimes) {
    EXPECT_TRUE(is_prime(2));
    EXPECT_TRUE(is_prime(13));
    EXPECT_TRUE(is_prime(97));
}

TEST(PrimeTest, KnownNonPrimes) {
    EXPECT_FALSE(is_prime(0));
    EXPECT_FALSE(is_prime(1));
    EXPECT_FALSE(is_prime(4));
    EXPECT_FALSE(is_prime(100));
}
# Build and run tests
cmake --build build
cd build && ctest --output-on-failure
# Or run directly:
./build/tests

Assertions

#include <gtest/gtest.h>
#include <vector>
#include <string>

TEST(AssertionDemo, BasicAssertions) {
    // EXPECT_* continues on failure; ASSERT_* stops the test
    EXPECT_EQ(1 + 1, 2);          // Equal
    EXPECT_NE(1, 2);               // Not equal
    EXPECT_LT(1, 2);               // Less than
    EXPECT_LE(1, 1);               // Less or equal
    EXPECT_GT(2, 1);               // Greater than
    EXPECT_GE(2, 2);               // Greater or equal

    // Boolean
    EXPECT_TRUE(1 == 1);
    EXPECT_FALSE(1 == 2);

    // String comparisons
    EXPECT_STREQ("hello", "hello");   // C-string equal
    EXPECT_STRNE("hello", "world");   // C-string not equal

    // Floating point (with tolerance)
    EXPECT_FLOAT_EQ(1.0f / 3.0f, 0.333333f);  // ~1e-7 tolerance
    EXPECT_DOUBLE_EQ(1.0 / 3.0, 0.333333333333333);
    EXPECT_NEAR(1.0 / 3.0, 0.333, 0.001);     // Custom tolerance

    // ASSERT_* — fatal failure, stops this test immediately
    int* ptr = new int(42);
    ASSERT_NE(ptr, nullptr);  // If null, don't continue
    EXPECT_EQ(*ptr, 42);      // Only runs if ASSERT passed
    delete ptr;
}

Test Fixtures

#include <gtest/gtest.h>
#include <vector>
#include <string>

// Test fixture — shared setup/teardown for related tests
class VectorTest : public ::testing::Test {
protected:
    std::vector<int> vec;

    void SetUp() override {
        // Runs before EACH test
        vec = {1, 2, 3, 4, 5};
    }

    void TearDown() override {
        // Runs after EACH test (optional)
        vec.clear();
    }
};

// Use TEST_F (not TEST) with fixtures
TEST_F(VectorTest, SizeIsCorrect) {
    EXPECT_EQ(vec.size(), 5);
}

TEST_F(VectorTest, PushBackIncreasesSize) {
    vec.push_back(6);
    EXPECT_EQ(vec.size(), 6);
    EXPECT_EQ(vec.back(), 6);
}

TEST_F(VectorTest, PopBackDecreasesSize) {
    vec.pop_back();
    EXPECT_EQ(vec.size(), 4);
}

TEST_F(VectorTest, ClearMakesEmpty) {
    vec.clear();
    EXPECT_TRUE(vec.empty());
}

Parameterized Tests

#include <gtest/gtest.h>
#include "math.hpp"

// Run the same test with different inputs
class PrimeTest : public ::testing::TestWithParam<std::pair<int, bool>> {};

TEST_P(PrimeTest, CheckPrimality) {
    auto [number, expected] = GetParam();
    EXPECT_EQ(is_prime(number), expected) << number << " primality check failed";
}

INSTANTIATE_TEST_SUITE_P(
    PrimeNumbers, PrimeTest,
    ::testing::Values(
        std::make_pair(0, false),
        std::make_pair(1, false),
        std::make_pair(2, true),
        std::make_pair(3, true),
        std::make_pair(4, false),
        std::make_pair(17, true),
        std::make_pair(100, false),
        std::make_pair(997, true)
    )
);

Testing Exceptions

#include <gtest/gtest.h>
#include <stdexcept>

int divide(int a, int b) {
    if (b == 0) throw std::invalid_argument("Division by zero");
    return a / b;
}

TEST(DivideTest, ThrowsOnZero) {
    EXPECT_THROW(divide(10, 0), std::invalid_argument);
    EXPECT_ANY_THROW(divide(10, 0));  // Any exception
    EXPECT_NO_THROW(divide(10, 2));   // No exception
}

Death Tests

#include <gtest/gtest.h>
#include <cassert>

void must_be_positive(int n) {
    assert(n > 0 && "n must be positive");
}

// Test that a function crashes as expected
TEST(DeathTest, AssertsOnNegative) {
    EXPECT_DEATH(must_be_positive(-1), "n must be positive");
}

// Test segfault
TEST(DeathTest, NullDereference) {
    int* p = nullptr;
    EXPECT_DEATH(*p = 42, "");  // Expects crash
}

Catch2 — The Alternative

// Catch2 is simpler — no macros for test registration
// Single header or CMake FetchContent

#include <catch2/catch_test_macros.hpp>

int add(int a, int b) { return a + b; }

TEST_CASE("Addition works correctly", "[math]") {
    REQUIRE(add(2, 3) == 5);       // Fatal assertion
    CHECK(add(0, 0) == 0);         // Non-fatal assertion
    CHECK(add(-1, 1) == 0);
}

TEST_CASE("Factorial", "[math]") {
    REQUIRE(factorial(0) == 1);
    REQUIRE(factorial(1) == 1);
    REQUIRE(factorial(5) == 120);
}

// Catch2 tags: run specific groups
// ./tests [math]        — run only [math] tagged tests
// ./tests ~[slow]       — skip [slow] tagged tests

Catch2 Sections and Generators

#include <catch2/catch_test_macros.hpp>
#include <vector>

TEST_CASE("Vector operations", "[vector]") {
    std::vector<int> vec = {1, 2, 3};

    // SECTION replaces fixtures — each section gets fresh setup
    SECTION("push_back increases size") {
        vec.push_back(4);
        REQUIRE(vec.size() == 4);
    }

    SECTION("pop_back decreases size") {
        vec.pop_back();
        REQUIRE(vec.size() == 2);
    }

    SECTION("clear empties the vector") {
        vec.clear();
        REQUIRE(vec.empty());
    }
    // Each SECTION starts with vec = {1, 2, 3}
}

// Generators (parameterized tests in Catch2)
#include <catch2/generators/catch_generators.hpp>

TEST_CASE("Generated tests", "[gen]") {
    auto i = GENERATE(1, 2, 3, 4, 5);
    REQUIRE(i > 0);
    REQUIRE(i < 6);
}

// BDD style
SCENARIO("Vector can grow", "[vector]") {
    GIVEN("an empty vector") {
        std::vector<int> v;
        WHEN("an element is added") {
            v.push_back(42);
            THEN("the size becomes 1") {
                REQUIRE(v.size() == 1);
            }
        }
    }
}

CMake Integration

# For Catch2:
FetchContent_Declare(
    Catch2
    GIT_REPOSITORY https://github.com/catchorg/Catch2.git
    GIT_TAG v3.5.2
)
FetchContent_MakeAvailable(Catch2)

add_executable(tests tests/test_math.cpp)
target_link_libraries(tests PRIVATE mathlib Catch2::Catch2WithMain)

include(CTest)
include(Catch)
catch_discover_tests(tests)
# Running tests
cmake --build build
cd build
ctest                          # Run all tests
ctest --output-on-failure      # Show output for failures
ctest -R PrimeTest             # Run tests matching pattern
ctest -j4                      # Parallel execution

Test-Driven Development

TDD follows a simple cycle: write a failing test, write the minimum code to make it pass, then refactor. This “Red-Green-Refactor” loop ensures every piece of functionality has a test from the start.

// Step 1: Write a failing test
TEST(StringUtils, TrimWhitespace) {
    EXPECT_EQ(trim("  hello  "), "hello");
    EXPECT_EQ(trim(""), "");
    EXPECT_EQ(trim("   "), "");
}

// Step 2: Write the minimum implementation
std::string trim(const std::string& s) {
    auto start = s.find_first_not_of(" 	
");
    if (start == std::string::npos) return "";
    auto end = s.find_last_not_of(" 	
");
    return s.substr(start, end - start + 1);
}

// Step 3: Refactor if needed, tests keep you safe

What to Test

Test the interface, not the implementation. Focus on: normal inputs (the happy path), edge cases (empty input, zero, max values, single element), error cases (invalid input, null pointers), and boundary conditions (off-by-one, integer overflow). Don’t test private methods directly — test them through the public interface.

Aim for tests that are fast (milliseconds), isolated (no shared state between tests), repeatable (same result every run), and self-checking (no manual inspection needed). These properties, often called FIRST (Fast, Isolated, Repeatable, Self-validating, Timely), make tests useful rather than burdensome.

Practice Exercises

Exercise 1: Write a StringCalculator class using TDD. Start with add("") returning 0, then add("1") returning 1, then add("1,2") returning 3. Add support for newline delimiters and custom delimiters.

Exercise 2: Create a Stack<T> template class with full Google Test coverage. Test push, pop, top, empty, and size. Test with different types (int, string, custom struct).

Exercise 3: Set up a Catch2 project using CMake FetchContent. Write BDD-style tests (SCENARIO/GIVEN/WHEN/THEN) for a shopping cart class.

Exercise 4: Write parameterized tests for a parse_int function that handles valid numbers, invalid strings, overflow, and leading/trailing whitespace.

Testing is what turns “I think it works” into “I know it works.” Professional C++ codebases have thousands of tests that run automatically on every commit. With debugging tools for finding bugs and tests for preventing regressions, you’re equipped for real-world C++ development. Next up: RAII and resource management — the pattern that makes C++ uniquely powerful.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *