C++ Unit Testing: Google Test & Catch2 Complete Guide 2026
Table of Contents
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.