Unit Testing (Google Test, Catch2) — Test Fixtures, Assertions, Mocks, TDD with C++
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
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).
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.
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.
How do you run a single test case in Google Test? Answer:
--gtest_filter=TestSuiteName.TestNameor--gtest_filter=*Pattern*.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
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