Skip to content

Unit Testing (Google Test, Catch2) — Test Fixtures, Assertions, Mocks, TDD with C++

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Unit Testing (Google Test, Catch2). We cover key concepts, practical examples, and best practices to help you master this topic.

C++ unit testing frameworks (Google Test, Catch2) provide assertion macros (EXPECT_EQ, ASSERT_TRUE), test fixtures for shared setup/teardown, and parameterized tests for thorough coverage with minimal code.

What You'll Learn

You will write test cases with Google Test's assertion macros, organize tests into suites and fixtures, use Catch2's BDD-style syntax (SCENARIO, GIVEN, WHEN, THEN), write parameterized tests that run with multiple inputs, create test doubles and mocks for dependency isolation, integrate tests with CMake/CTest, and apply Test-Driven Development (TDD) to C++.

Why It Matters

Untested C++ code is fragile — a single pointer error or undefined behavior can take hours to debug. Unit tests catch regressions instantly, document expected behavior, and enable confident Refactoring. C++ projects like LLVM, Qt, and Boost have millions of lines of tests. Learning a testing framework is essential for professional C++ development.

Learning Path

graph LR
    A["65: Build Systems (CMake)"] --> B["66: Unit Testing"]
    B --> C["67: Debugging (GDB)"]
    C --> D["68: Performance Profiling"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Google Test Basics

Simple test cases with EXPECT_ and ASSERT_ macros.

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

// Function to test
int add(int a, int b) {
    return a + b;
}

std::string greet(const std::string& name) {
    return "Hello, " + name + "!";
}

// Simple test
TEST(MathTest, Add) {
    EXPECT_EQ(add(2, 3), 5);
    EXPECT_EQ(add(-1, 1), 0);
    EXPECT_EQ(add(0, 0), 0);
    EXPECT_NE(add(2, 2), 5);  // Not equal
    EXPECT_LT(add(1, 1), 3);  // Less than
    EXPECT_GT(add(1, 1), 1);  // Greater than
}

TEST(StringTest, Greet) {
    EXPECT_EQ(greet("World"), "Hello, World!");
    EXPECT_EQ(greet(""), "Hello, !");
    EXPECT_TRUE(greet("Alice").find("Alice") != std::string::npos);
}

// ASSERT stops on failure, EXPECT continues
TEST(MathTest, Division) {
    ASSERT_EQ(10 / 2, 5);     // If this fails, test stops
    EXPECT_EQ(10 / 3, 3);     // Integer division
    // ASSERT_EQ(10 / 0, 0);  // Would crash — test death
}

int main(int argc, char** argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

Test Fixtures

Share setup and teardown across test cases.

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

// Class to test
class Stack {
    std::vector<int> data_;
public:
    void push(int x) { data_.push_back(x); }
    int pop() {
        int val = data_.back();
        data_.pop_back();
        return val;
    }
    int top() const { return data_.back(); }
    bool empty() const { return data_.empty(); }
    size_t size() const { return data_.size(); }
};

// Test fixture
class StackTest : public ::testing::Test {
protected:
    Stack stack;

    void SetUp() override {
        // Called before each test
        stack.push(10);
        stack.push(20);
        stack.push(30);
    }

    void TearDown() override {
        // Called after each test (cleanup)
    }
};

// Tests use the fixture
TEST_F(StackTest, PopReturnsLastElement) {
    EXPECT_EQ(stack.pop(), 30);
    EXPECT_EQ(stack.pop(), 20);
    EXPECT_EQ(stack.pop(), 10);
}

TEST_F(StackTest, SizeDecreasesAfterPop) {
    EXPECT_EQ(stack.size(), 3);
    stack.pop();
    EXPECT_EQ(stack.size(), 2);
    stack.pop();
    EXPECT_EQ(stack.size(), 1);
}

TEST_F(StackTest, EmptyAfterAllPops) {
    stack.pop();
    stack.pop();
    stack.pop();
    EXPECT_TRUE(stack.empty());
}

TEST_F(StackTest, TopReturnsLastWithoutRemoving) {
    EXPECT_EQ(stack.top(), 30);
    EXPECT_EQ(stack.size(), 3);  // size unchanged
}

Parameterized Tests

Run the same test with multiple inputs.

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

bool isPalindrome(const std::string& s) {
    int left = 0, right = static_cast<int>(s.size()) - 1;
    while (left < right) {
        if (s[left] != s[right]) return false;
        ++left;
        --right;
    }
    return true;
}

struct PalindromeTestCase {
    std::string input;
    bool expected;
};

class PalindromeTest :
    public ::testing::TestWithParam<PalindromeTestCase> {};

TEST_P(PalindromeTest, ChecksCorrectly) {
    auto [input, expected] = GetParam();
    EXPECT_EQ(isPalindrome(input), expected);
}

INSTANTIATE_TEST_SUITE_P(
    VariousStrings,
    PalindromeTest,
    ::testing::Values(
        PalindromeTestCase{"", true},
        PalindromeTestCase{"a", true},
        PalindromeTestCase{"aa", true},
        PalindromeTestCase{"aba", true},
        PalindromeTestCase{"racecar", true},
        PalindromeTestCase{"hello", false},
        PalindromeTestCase{"ab", false},
        PalindromeTestCase{"Aba", false}  // Case-sensitive
    )
);

Catch2 — BDD-Style Tests

Catch2 has a different syntax with BDD support.

// Catch2 v3 — header-only or compiled
// #include <catch2/catch_test_macros.hpp>
// #include <catch2/catch_session.hpp>

// For illustration (conceptual — requires Catch2 installed)
int multiply(int a, int b) {
    return a * b;
}

/*
TEST_CASE("Multiplication works", "[math]") {
    REQUIRE(multiply(2, 3) == 6);
    REQUIRE(multiply(-1, 5) == -5);
    REQUIRE(multiply(0, 100) == 0);
}

TEST_CASE("Division by zero throws", "[math][exception]") {
    REQUIRE_THROWS_AS(divide(1, 0), std::invalid_argument);
}

// BDD style
SCENARIO("Shopping cart total calculation", "[cart]") {
    GIVEN("An empty cart") {
        Cart cart;

        WHEN("I add items worth $10 and $20") {
            cart.addItem(Item("book", 10.0));
            cart.addItem(Item("pen", 20.0));

            THEN("The total should be $30") {
                REQUIRE(cart.total() == Approx(30.0));
            }
        }

        WHEN("The cart is empty") {
            THEN("The total should be $0") {
                REQUIRE(cart.total() == Approx(0.0));
            }
        }
    }
}
*/

Matchers and Predicates

Advanced assertions with Google Test matchers.

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

using ::testing::_;
using ::testing::AllOf;
using ::testing::Gt;
using ::testing::Lt;
using ::testing::Contains;
using ::testing::ElementsAre;
using ::testing::UnorderedElementsAre;
using ::testing::Pair;
using ::testing::StartsWith;

TEST(MatchersTest, ContainerMatchers) {
    std::vector<int> v = {1, 2, 3, 4, 5};

    // Element matchers
    EXPECT_THAT(v, Contains(3));
    EXPECT_THAT(v, ElementsAre(1, 2, 3, 4, 5));
    EXPECT_THAT(v, ElementsAre(Gt(0), Gt(1), Gt(2), Gt(3), Gt(4)));

    // Unordered
    std::vector<int> shuffled = {3, 1, 5, 2, 4};
    EXPECT_THAT(shuffled, UnorderedElementsAre(1, 2, 3, 4, 5));
}

TEST(MatchersTest, StringMatchers) {
    std::string s = "Hello, World!";
    EXPECT_THAT(s, StartsWith("Hello"));
    EXPECT_THAT(s, ::testing::EndsWith("!"));
    EXPECT_THAT(s, ::testing::HasSubstr("World"));
}

TEST(MatchersTest, MapMatchers) {
    std::map<int, std::string> m = {{1, "one"}, {2, "two"}};
    EXPECT_THAT(m, Contains(Pair(1, "one")));
    EXPECT_THAT(m, Contains(Pair(2, "two")));
}

TEST(MatchersTest, FloatingPoint) {
    double a = 0.1 + 0.2;  // Not exactly 0.3
    EXPECT_DOUBLE_EQ(a, 0.3);  // Uses ULP comparison
    EXPECT_NEAR(a, 0.3, 1e-10);
}

Mocks with Google Mock

Isolate code under test by mocking dependencies.

#include <gmock/gmock.h>
#include <memory>
#include <string>

// Interface to mock
class Database {
public:
    virtual ~Database() = default;
    virtual std::string getUserName(int id) = 0;
    virtual void saveUser(int id, const std::string& name) = 0;
};

// Mock class
class MockDatabase : public Database {
public:
    MOCK_METHOD(std::string, getUserName, (int id), (override));
    MOCK_METHOD(void, saveUser, (int id, const std::string& name), (override));
};

// Code under test
class UserService {
    Database& db_;
public:
    explicit UserService(Database& db) : db_(db) {}

    std::string getUser(int id) {
        std::string name = db_.getUserName(id);
        if (name.empty()) {
            return "User not found";
        }
        return "Hello, " + name;
    }

    void createUser(int id, const std::string& name) {
        if (name.empty()) {
            throw std::invalid_argument("Name cannot be empty");
        }
        db_.saveUser(id, name);
    }
};

using ::testing::Return;
using ::testing::Throw;

TEST(UserServiceTest, GetExistingUser) {
    MockDatabase db;
    UserService service(db);

    EXPECT_CALL(db, getUserName(1))
        .WillOnce(Return("Alice"));

    std::string result = service.getUser(1);
    EXPECT_EQ(result, "Hello, Alice");
}

TEST(UserServiceTest, GetMissingUser) {
    MockDatabase db;
    UserService service(db);

    EXPECT_CALL(db, getUserName(42))
        .WillOnce(Return(""));

    std::string result = service.getUser(42);
    EXPECT_EQ(result, "User not found");
}

TEST(UserServiceTest, CreateUserValidatesName) {
    MockDatabase db;
    UserService service(db);

    EXPECT_CALL(db, saveUser(_, _)).Times(0);  // Should not be called

    EXPECT_THROW(service.createUser(1, ""), std::invalid_argument);
}

TEST(UserServiceTest, CreateUserSaves) {
    MockDatabase db;
    UserService service(db);

    EXPECT_CALL(db, saveUser(1, "Bob")).Times(1);

    service.createUser(1, "Bob");
}

Death Tests

Test that code triggers a fatal assertion.

#include <gtest/gtest.h>

void crashFunction() {
    ASSERT_TRUE(false) << "This should crash the test";
}

void dieIfNull(void* ptr) {
    if (!ptr) {
        fprintf(stderr, "Fatal: null pointer\n");
        std::abort();
    }
}

TEST(DeathTest, AbortsOnNull) {
    EXPECT_DEATH(dieIfNull(nullptr), "Fatal: null pointer");
}

TEST(DeathTest, CrashFunction) {
    // Death tests check for assertion failures
    // ASSERT_DEATH(crashFunction(), "This should crash");
}

// Death tests have limitations:
// - Run in a subprocess (fork)
// - Cannot use death tests with thread-safe death tests on some platforms
// - ASSERT_EXIT for custom exit codes

Integrating with CMake/CTest

cmake_minimum_required(VERSION 3.20)
project(TestProject)

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

enable_testing()

# Library to test
add_library(my_lib src/calculator.cpp src/parser.cpp)
target_include_directories(my_lib PUBLIC include)

# Test executable
add_executable(my_tests
    tests/test_calculator.cpp
    tests/test_parser.cpp
)
target_link_libraries(my_tests PRIVATE my_lib gtest gtest_main)

# Register with CTest
include(GoogleTest)
gtest_discover_tests(my_tests)

# Run: cmake --build build && ctest --test-dir build

Common Mistakes

Mistake 1: Testing implementation details instead of behavior

Test public interfaces, not private methods. If a private method needs testing, extract it.

Mistake 2: Brittle floating-point comparisons

EXPECT_EQ(0.1 + 0.2, 0.3);  // May fail due to floating-point precision

Use EXPECT_DOUBLE_EQ or EXPECT_NEAR.

Mistake 3: Tests that depend on each other

Tests should be independent and runnable in any order. Use SetUp for fresh state.

Mistake 4: Not testing edge cases

Test empty inputs, negative values, maximum values, null pointers, boundary conditions.

Mistake 5: Tests that require manual verification

Every test should have an automatic assertion. If a test just runs code without ASSERT or EXPECT, it's not testing anything.

Practice Questions

  1. What is the difference between EXPECT_EQ and ASSERT_EQ? Answer: EXPECT_EQ continues after failure (non-fatal). ASSERT_EQ stops the test immediately (fatal).

  2. What is a test fixture and when do you use it? Answer: A test fixture class (inheriting from ::testing::Test) with SetUp/TearDown methods, used when multiple tests share setup code.

  3. What is a mock in Google Mock? Answer: A mock is a test double that records expected calls and returns predefined values. It isolates the code under test from its dependencies.

  4. How do you run a single test case in Google Test? Answer: --gtest_filter=TestSuiteName.TestName or --gtest_filter=*Pattern*.

  5. Write a parameterized test for a function that returns true for even numbers. Answer: Use TEST_P with TestWithParam and INSTANTIATE_TEST_SUITE_P with various even/odd inputs.

FAQ

What unit testing frameworks are available for C++

Google Test (gtest) and Catch2 are the most popular. CppUnit, Boost.Test, and doctest are also widely used.

What is the difference between Google Test and Catch2

Google Test has richer matchers, mocks (GMock), and parameterized tests. Catch2 has BDD-style syntax (SCENARIO/GIVEN/WHEN/THEN) and is simpler for small projects.

What is test-driven development (TDD)

TDD is writing tests before code: Red (test fails), Green (make test pass), Refactor (improve code while tests stay green).

How do I mock external dependencies in C++

Define interfaces for dependencies, create mock implementations using Google Mock (MOCK_METHOD), inject them into the code under test.

How do I organize test files in a project

Place tests in a tests/ directory mirroring src/ structure. Name test files test_.cpp or _test.cpp.

Mini Project

Write a test suite for a simple Bank Account class using TDD:

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

// Your BankAccount class (write tests first!)
class BankAccount {
public:
    BankAccount(std::string owner, double balance);
    void deposit(double amount);
    void withdraw(double amount);
    double getBalance() const;
    std::string getOwner() const;
    void transferTo(BankAccount& other, double amount);
};

// Your tests should cover:
// 1. Creating accounts with initial balance
// 2. Depositing money
// 3. Withdrawing money (including insufficient funds)
// 4. Transfer between accounts
// 5. Preventing negative deposits
// 6. Edge cases: zero amounts, large amounts

This project demonstrates the C++ testing workflow used in real projects — write tests, implement, verify. Compare with Java's JUnit framework which follows the same patterns.

What's Next

You now write unit tests for C++ code. Next, you will learn debugging with GDB — finding and fixing bugs in compiled C++ programs using breakpoints, backtraces, and memory inspection.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro