Skip to content

C++ Unit Testing

Overview

Unit testing is an important practice in software development, used to verify that the smallest testable units of code work as expected. C++ has multiple testing frameworks available. This chapter introduces the Google Test framework, Test-Driven Development (TDD), Mock Objects, and other testing techniques.

🧪 Google Test Basics

Environment Setup and Basic Usage

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

// Class under test
class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
    
    int subtract(int a, int b) {
        return a - b;
    }
    
    int multiply(int a, int b) {
        return a * b;
    }
    
    double divide(double a, double b) {
        if (b == 0) {
            throw std::invalid_argument("Division by zero");
        }
        return a / b;
    }
    
    bool isPrime(int n) {
        if (n <= 1) return false;
        if (n <= 3) return true;
        if (n % 2 == 0 || n % 3 == 0) return false;
        
        for (int i = 5; i * i <= n; i += 6) {
            if (n % i == 0 || n % (i + 2) == 0) {
                return false;
            }
        }
        return true;
    }
};

// Basic test cases
TEST(CalculatorTest, Addition) {
    Calculator calc;
    
    EXPECT_EQ(calc.add(2, 3), 5);
    EXPECT_EQ(calc.add(-1, 1), 0);
    EXPECT_EQ(calc.add(0, 0), 0);
}

TEST(CalculatorTest, Subtraction) {
    Calculator calc;
    
    EXPECT_EQ(calc.subtract(5, 3), 2);
    EXPECT_EQ(calc.subtract(1, 1), 0);
    EXPECT_EQ(calc.subtract(0, 5), -5);
}

TEST(CalculatorTest, Multiplication) {
    Calculator calc;
    
    EXPECT_EQ(calc.multiply(3, 4), 12);
    EXPECT_EQ(calc.multiply(-2, 3), -6);
    EXPECT_EQ(calc.multiply(0, 5), 0);
}

TEST(CalculatorTest, Division) {
    Calculator calc;
    
    EXPECT_DOUBLE_EQ(calc.divide(10, 2), 5.0);
    EXPECT_DOUBLE_EQ(calc.divide(7, 2), 3.5);
    
    // Test exception
    EXPECT_THROW(calc.divide(5, 0), std::invalid_argument);
}

TEST(CalculatorTest, PrimeCheck) {
    Calculator calc;
    
    // Prime number tests
    EXPECT_TRUE(calc.isPrime(2));
    EXPECT_TRUE(calc.isPrime(3));
    EXPECT_TRUE(calc.isPrime(17));
    
    // Non-prime number tests
    EXPECT_FALSE(calc.isPrime(1));
    EXPECT_FALSE(calc.isPrime(4));
    EXPECT_FALSE(calc.isPrime(15));
}

Assertions and Expectations

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

class StringUtils {
public:
    static std::string toUpper(const std::string& str) {
        std::string result = str;
        std::transform(result.begin(), result.end(), result.begin(), ::toupper);
        return result;
    }
    
    static bool contains(const std::string& str, const std::string& substr) {
        return str.find(substr) != std::string::npos;
    }
    
    static std::vector<std::string> split(const std::string& str, char delimiter) {
        std::vector<std::string> result;
        std::stringstream ss(str);
        std::string item;
        
        while (std::getline(ss, item, delimiter)) {
            result.push_back(item);
        }
        
        return result;
    }
};

// Various assertion types
TEST(AssertionTest, BasicAssertions) {
    // Boolean assertions
    EXPECT_TRUE(true);
    EXPECT_FALSE(false);
    
    // Equality assertions
    EXPECT_EQ(42, 42);
    EXPECT_NE(42, 43);
    
    // Comparison assertions
    EXPECT_LT(1, 2);    // less than
    EXPECT_LE(2, 2);    // less equal
    EXPECT_GT(3, 2);    // greater than
    EXPECT_GE(3, 3);    // greater equal
}

TEST(AssertionTest, StringAssertions) {
    std::string str = "Hello World";
    
    EXPECT_STREQ("Hello", "Hello");
    EXPECT_STRNE("Hello", "World");
    EXPECT_STRCASEEQ("hello", "HELLO");
    
    // String contains
    EXPECT_PRED2([](const std::string& str, const std::string& substr) {
        return str.find(substr) != std::string::npos;
    }, str, "World");
}

TEST(AssertionTest, FloatingPointAssertions) {
    double a = 1.0;
    double b = 0.1 * 10;
    
    // Floating point comparison
    EXPECT_DOUBLE_EQ(a, b);
    EXPECT_NEAR(3.14159, 3.14, 0.01);
    
    float f1 = 1.0f;
    float f2 = 0.1f * 10;
    EXPECT_FLOAT_EQ(f1, f2);
}

TEST(StringUtilsTest, ToUpperCase) {
    EXPECT_EQ(StringUtils::toUpper("hello"), "HELLO");
    EXPECT_EQ(StringUtils::toUpper(""), "");
    EXPECT_EQ(StringUtils::toUpper("MiXeD"), "MIXED");
}

TEST(StringUtilsTest, Contains) {
    EXPECT_TRUE(StringUtils::contains("hello world", "world"));
    EXPECT_FALSE(StringUtils::contains("hello", "world"));
    EXPECT_TRUE(StringUtils::contains("", ""));
}

TEST(StringUtilsTest, Split) {
    auto result = StringUtils::split("a,b,c", ',');
    
    ASSERT_EQ(result.size(), 3);
    EXPECT_EQ(result[0], "a");
    EXPECT_EQ(result[1], "b");
    EXPECT_EQ(result[2], "c");
}

🔧 Test Fixtures (Test Fixtures)

Class-Level Fixtures

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

class NumberList {
private:
    std::vector<int> numbers_;
    
public:
    void add(int number) {
        numbers_.push_back(number);
    }
    
    void remove(int number) {
        numbers_.erase(
            std::remove(numbers_.begin(), numbers_.end(), number),
            numbers_.end()
        );
    }
    
    bool contains(int number) const {
        return std::find(numbers_.begin(), numbers_.end(), number) != numbers_.end();
    }
    
    size_t size() const {
        return numbers_.size();
    }
    
    void clear() {
        numbers_.clear();
    }
    
    std::vector<int> getSorted() const {
        std::vector<int> sorted = numbers_;
        std::sort(sorted.begin(), sorted.end());
        return sorted;
    }
    
    int getMax() const {
        if (numbers_.empty()) {
            throw std::runtime_error("List is empty");
        }
        return *std::max_element(numbers_.begin(), numbers_.end());
    }
};

// Test fixture class
class NumberListTest : public ::testing::Test {
protected:
    void SetUp() override {
        // Execute before each test
        list.add(1);
        list.add(3);
        list.add(2);
    }
    
    void TearDown() override {
        // Execute after each test
        list.clear();
    }
    
    NumberList list;
};

TEST_F(NumberListTest, InitialState) {
    EXPECT_EQ(list.size(), 3);
    EXPECT_TRUE(list.contains(1));
    EXPECT_TRUE(list.contains(2));
    EXPECT_TRUE(list.contains(3));
}

TEST_F(NumberListTest, AddNumber) {
    list.add(4);
    
    EXPECT_EQ(list.size(), 4);
    EXPECT_TRUE(list.contains(4));
}

TEST_F(NumberListTest, RemoveNumber) {
    list.remove(2);
    
    EXPECT_EQ(list.size(), 2);
    EXPECT_FALSE(list.contains(2));
    EXPECT_TRUE(list.contains(1));
    EXPECT_TRUE(list.contains(3));
}

TEST_F(NumberListTest, GetSorted) {
    auto sorted = list.getSorted();
    
    ASSERT_EQ(sorted.size(), 3);
    EXPECT_EQ(sorted[0], 1);
    EXPECT_EQ(sorted[1], 2);
    EXPECT_EQ(sorted[2], 3);
}

TEST_F(NumberListTest, GetMax) {
    EXPECT_EQ(list.getMax(), 3);
    
    list.add(5);
    EXPECT_EQ(list.getMax(), 5);
}

TEST_F(NumberListTest, EmptyListMax) {
    NumberList emptyList;
    EXPECT_THROW(emptyList.getMax(), std::runtime_error);
}

Parameterized Tests

cpp
#include <gtest/gtest.h>
#include <cmath>

// Function under test
bool isPerfectSquare(int n) {
    if (n < 0) return false;
    int root = static_cast<int>(std::sqrt(n));
    return root * root == n;
}

// Parameterized test
class PerfectSquareTest : public ::testing::TestWithParam<std::pair<int, bool>> {
};

TEST_P(PerfectSquareTest, CheckPerfectSquare) {
    auto param = GetParam();
    int number = param.first;
    bool expected = param.second;
    
    EXPECT_EQ(isPerfectSquare(number), expected);
}

// Test data
INSTANTIATE_TEST_SUITE_P(
    PerfectSquareValues,
    PerfectSquareTest,
    ::testing::Values(
        std::make_pair(0, true),   // 0 is a perfect square
        std::make_pair(1, true),   // 1 = 1²
        std::make_pair(4, true),   // 4 = 2²
        std::make_pair(9, true),   // 9 = 3²
        std::make_pair(16, true),  // 16 = 4²
        std::make_pair(2, false),  // 2 is not a perfect square
        std::make_pair(3, false),  // 3 is not a perfect square
        std::make_pair(5, false),  // 5 is not a perfect square
        std::make_pair(-1, false)  // Negative numbers are not perfect squares
    )
);

// Type-parameterized test
template<typename T>
class ContainerTest : public ::testing::Test {
public:
    using Container = T;
    Container container_;
};

using ContainerTypes = ::testing::Types<
    std::vector<int>,
    std::list<int>,
    std::deque<int>
>;

TYPED_TEST_SUITE(ContainerTest, ContainerTypes);

TYPED_TEST(ContainerTest, EmptyContainer) {
    EXPECT_TRUE(this->container_.empty());
    EXPECT_EQ(this->container_.size(), 0);
}

TYPED_TEST(ContainerTest, AddElements) {
    this->container_.push_back(1);
    this->container_.push_back(2);
    
    EXPECT_FALSE(this->container_.empty());
    EXPECT_EQ(this->container_.size(), 2);
}

🎭 Mock Objects (Mock Objects)

Google Mock Basics

cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>

// Interface definition
class DatabaseInterface {
public:
    virtual ~DatabaseInterface() = default;
    virtual bool connect(const std::string& connectionString) = 0;
    virtual std::vector<std::string> query(const std::string& sql) = 0;
    virtual bool execute(const std::string& sql) = 0;
    virtual void disconnect() = 0;
};

// Mock class
class MockDatabase : public DatabaseInterface {
public:
    MOCK_METHOD(bool, connect, (const std::string& connectionString), (override));
    MOCK_METHOD(std::vector<std::string>, query, (const std::string& sql), (override));
    MOCK_METHOD(bool, execute, (const std::string& sql), (override));
    MOCK_METHOD(void, disconnect, (), (override));
};

// Service class using database
class UserService {
private:
    DatabaseInterface* database_;
    
public:
    UserService(DatabaseInterface* database) : database_(database) {}
    
    bool initialize(const std::string& connectionString) {
        return database_->connect(connectionString);
    }
    
    std::vector<std::string> getUsernames() {
        return database_->query("SELECT username FROM users");
    }
    
    bool createUser(const std::string& username, const std::string& email) {
        std::string sql = "INSERT INTO users (username, email) VALUES ('" + 
                         username + "', '" + email + "')";
        return database_->execute(sql);
    }
    
    void cleanup() {
        database_->disconnect();
    }
};

// Mock test
class UserServiceTest : public ::testing::Test {
protected:
    void SetUp() override {
        service = std::make_unique<UserService>(&mockDatabase);
    }
    
    MockDatabase mockDatabase;
    std::unique_ptr<UserService> service;
};

TEST_F(UserServiceTest, Initialize) {
    // Set expectations
    EXPECT_CALL(mockDatabase, connect("test_connection"))
        .WillOnce(::testing::Return(true));
    
    // Execute test
    bool result = service->initialize("test_connection");
    
    // Verify result
    EXPECT_TRUE(result);
}

TEST_F(UserServiceTest, GetUsernames) {
    std::vector<std::string> expectedUsers = {"alice", "bob", "charlie"};
    
    EXPECT_CALL(mockDatabase, query("SELECT username FROM users"))
        .WillOnce(::testing::Return(expectedUsers));
    
    auto users = service->getUsernames();
    
    EXPECT_EQ(users, expectedUsers);
}

TEST_F(UserServiceTest, CreateUser) {
    std::string expectedSql = "INSERT INTO users (username, email) VALUES ('john', 'john@example.com')";
    
    EXPECT_CALL(mockDatabase, execute(expectedSql))
        .WillOnce(::testing::Return(true));
    
    bool result = service->createUser("john", "john@example.com");
    
    EXPECT_TRUE(result);
}

TEST_F(UserServiceTest, Cleanup) {
    EXPECT_CALL(mockDatabase, disconnect())
        .Times(1);
    
    service->cleanup();
}

Advanced Mock Techniques

cpp
#include <gmock/gmock.h>

// File system interface
class FileSystemInterface {
public:
    virtual ~FileSystemInterface() = default;
    virtual bool fileExists(const std::string& path) = 0;
    virtual std::string readFile(const std::string& path) = 0;
    virtual bool writeFile(const std::string& path, const std::string& content) = 0;
    virtual bool deleteFile(const std::string& path) = 0;
};

class MockFileSystem : public FileSystemInterface {
public:
    MOCK_METHOD(bool, fileExists, (const std::string& path), (override));
    MOCK_METHOD(std::string, readFile, (const std::string& path), (override));
    MOCK_METHOD(bool, writeFile, (const std::string& path, const std::string& content), (override));
    MOCK_METHOD(bool, deleteFile, (const std::string& path), (override));
};

// Configuration manager
class ConfigManager {
private:
    FileSystemInterface* fileSystem_;
    std::string configPath_;
    
public:
    ConfigManager(FileSystemInterface* fs, const std::string& path) 
        : fileSystem_(fs), configPath_(path) {}
    
    bool loadConfig() {
        if (!fileSystem_->fileExists(configPath_)) {
            return false;
        }
        
        std::string content = fileSystem_->readFile(configPath_);
        return !content.empty();
    }
    
    bool saveConfig(const std::string& config) {
        return fileSystem_->writeFile(configPath_, config);
    }
    
    bool resetConfig() {
        if (fileSystem_->fileExists(configPath_)) {
            return fileSystem_->deleteFile(configPath_);
        }
        return true;
    }
};

class ConfigManagerTest : public ::testing::Test {
protected:
    void SetUp() override {
        manager = std::make_unique<ConfigManager>(&mockFileSystem, "config.txt");
    }
    
    MockFileSystem mockFileSystem;
    std::unique_ptr<ConfigManager> manager;
};

TEST_F(ConfigManagerTest, LoadConfigFileExists) {
    // Use matchers
    EXPECT_CALL(mockFileSystem, fileExists(::testing::_))
        .WillOnce(::testing::Return(true));
    
    EXPECT_CALL(mockFileSystem, readFile("config.txt"))
        .WillOnce(::testing::Return("config_content"));
    
    EXPECT_TRUE(manager->loadConfig());
}

TEST_F(ConfigManagerTest, LoadConfigFileNotExists) {
    EXPECT_CALL(mockFileSystem, fileExists("config.txt"))
        .WillOnce(::testing::Return(false));
    
    // Should not call readFile
    EXPECT_CALL(mockFileSystem, readFile(::testing::_))
        .Times(0);
    
    EXPECT_FALSE(manager->loadConfig());
}

TEST_F(ConfigManagerTest, SaveConfig) {
    std::string config = "new_config";
    
    EXPECT_CALL(mockFileSystem, writeFile("config.txt", config))
        .WillOnce(::testing::Return(true));
    
    EXPECT_TRUE(manager->saveConfig(config));
}

TEST_F(ConfigManagerTest, ResetConfigMultipleCalls) {
    // First call returns true (file exists), second returns false (file doesn't exist)
    EXPECT_CALL(mockFileSystem, fileExists("config.txt"))
        .WillOnce(::testing::Return(true))
        .WillOnce(::testing::Return(false));
    
    EXPECT_CALL(mockFileSystem, deleteFile("config.txt"))
        .WillOnce(::testing::Return(true));
    
    // First reset
    EXPECT_TRUE(manager->resetConfig());
    
    // Second reset (file already doesn't exist)
    EXPECT_TRUE(manager->resetConfig());
}

🔄 Test-Driven Development (TDD)

TDD Practice Example

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

// Shopping cart class TDD development
class ShoppingCart {
private:
    std::unordered_map<std::string, std::pair<double, int>> items_; // Product name -> (price, quantity)
    
public:
    void addItem(const std::string& name, double price, int quantity = 1) {
        if (items_.find(name) != items_.end()) {
            items_[name].second += quantity;
        } else {
            items_[name] = {price, quantity};
        }
    }
    
    void removeItem(const std::string& name) {
        items_.erase(name);
    }
    
    void updateQuantity(const std::string& name, int quantity) {
        if (items_.find(name) != items_.end()) {
            if (quantity <= 0) {
                removeItem(name);
            } else {
                items_[name].second = quantity;
            }
        }
    }
    
    double getTotal() const {
        double total = 0.0;
        for (const auto& item : items_) {
            total += item.second.first * item.second.second;
        }
        return total;
    }
    
    int getItemCount() const {
        int count = 0;
        for (const auto& item : items_) {
            count += item.second.second;
        }
        return count;
    }
    
    bool isEmpty() const {
        return items_.empty();
    }
    
    bool hasItem(const std::string& name) const {
        return items_.find(name) != items_.end();
    }
    
    int getItemQuantity(const std::string& name) const {
        auto it = items_.find(name);
        return (it != items_.end()) ? it->second.second : 0;
    }
};

// TDD Step 1: Red - Write failing test
TEST(ShoppingCartTest, EmptyCartInitially) {
    ShoppingCart cart;
    
    EXPECT_TRUE(cart.isEmpty());
    EXPECT_EQ(cart.getItemCount(), 0);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 0.0);
}

// TDD Step 2: Green - Make test pass
// TDD Step 3: Refactor - Improve code quality

TEST(ShoppingCartTest, AddSingleItem) {
    ShoppingCart cart;
    
    cart.addItem("Apple", 1.50);
    
    EXPECT_FALSE(cart.isEmpty());
    EXPECT_EQ(cart.getItemCount(), 1);
    EXPECT_TRUE(cart.hasItem("Apple"));
    EXPECT_EQ(cart.getItemQuantity("Apple"), 1);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 1.50);
}

TEST(ShoppingCartTest, AddMultipleItems) {
    ShoppingCart cart;
    
    cart.addItem("Apple", 1.50, 3);
    cart.addItem("Banana", 0.80, 2);
    
    EXPECT_EQ(cart.getItemCount(), 5);
    EXPECT_EQ(cart.getItemQuantity("Apple"), 3);
    EXPECT_EQ(cart.getItemQuantity("Banana"), 2);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 6.10); // 3*1.50 + 2*0.80
}

TEST(ShoppingCartTest, AddSameItemTwice) {
    ShoppingCart cart;
    
    cart.addItem("Apple", 1.50, 2);
    cart.addItem("Apple", 1.50, 1);
    
    EXPECT_EQ(cart.getItemQuantity("Apple"), 3);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 4.50);
}

TEST(ShoppingCartTest, RemoveItem) {
    ShoppingCart cart;
    
    cart.addItem("Apple", 1.50, 2);
    cart.addItem("Banana", 0.80);
    
    cart.removeItem("Apple");
    
    EXPECT_FALSE(cart.hasItem("Apple"));
    EXPECT_TRUE(cart.hasItem("Banana"));
    EXPECT_EQ(cart.getItemCount(), 1);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 0.80);
}

TEST(ShoppingCartTest, UpdateQuantity) {
    ShoppingCart cart;
    
    cart.addItem("Apple", 1.50, 2);
    
    cart.updateQuantity("Apple", 5);
    EXPECT_EQ(cart.getItemQuantity("Apple"), 5);
    EXPECT_DOUBLE_EQ(cart.getTotal(), 7.50);
    
    // Updating to 0 should remove the item
    cart.updateQuantity("Apple", 0);
    EXPECT_FALSE(cart.hasItem("Apple"));
    EXPECT_TRUE(cart.isEmpty());
}

🚀 Advanced Testing Techniques

Death Tests

cpp
#include <gtest/gtest.h>

void criticalFunction(int value) {
    if (value < 0) {
        abort();
    }
    // Normal logic
}

void assertFunction(bool condition) {
    assert(condition);
}

// Death tests
TEST(DeathTest, CriticalFunctionAborts) {
    EXPECT_DEATH(criticalFunction(-1), ".*");
}

TEST(DeathTest, AssertionFailure) {
    EXPECT_DEATH(assertFunction(false), ".*");
}

// Assertions only in DEBUG mode
#ifdef NDEBUG
TEST(DeathTest, AssertionInRelease) {
    // In Release mode, assertions are disabled
    EXPECT_NO_FATAL_FAILURE(assertFunction(false));
}
#endif

Performance Test Integration

cpp
#include <gtest/gtest.h>
#include <chrono>

class PerformanceTest : public ::testing::Test {
protected:
    template<typename Func>
    double measureTime(Func func) {
        auto start = std::chrono::high_resolution_clock::now();
        func();
        auto end = std::chrono::high_resolution_clock::now();
        
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        return static_cast<double>(duration.count());
    }
};

TEST_F(PerformanceTest, SortingPerformance) {
    const size_t SIZE = 10000;
    std::vector<int> data(SIZE);
    std::iota(data.begin(), data.end(), 0);
    std::shuffle(data.begin(), data.end(), std::mt19937{std::random_device{}()});
    
    auto sortTime = measureTime([&]() {
        std::sort(data.begin(), data.end());
    });
    
    // Expect sorting time to be less than threshold (unit: microseconds)
    EXPECT_LT(sortTime, 10000.0) << "Sorting took too long: " << sortTime << " microseconds";
    
    // Verify sorting actually happened
    EXPECT_TRUE(std::is_sorted(data.begin(), data.end()));
}

Running and Configuration

cpp
// main function example
int main(int argc, char** argv) {
    // Initialize Google Test
    ::testing::InitGoogleTest(&argc, argv);
    
    // Run all tests
    return RUN_ALL_TESTS();
}

// Test filter examples
// Run only Calculator-related tests: --gtest_filter=Calculator*
// Exclude performance tests: --gtest_filter=-*Performance*
// Repeat 3 times: --gtest_repeat=3
// Verbose output: --gtest_verbose

Summary

Testing Framework Features

  • Google Test: Rich assertions, fixture support
  • Google Mock: Powerful mock object functionality
  • Parameterized Tests: Data-driven testing
  • Death Tests: Verify program crash behavior

Testing Types

Test TypePurposeTools
Unit TestingVerify single function/classGoogle Test
Integration TestingVerify component interactionMock Objects
Performance TestingVerify performance metricsTimers
Parameterized TestingBatch test dataTEST_P

Best Practices

  • TDD Development Process: Red-Green-Refactor
  • Test Isolation: Each test runs independently
  • Moderate Mock Usage: Only mock necessary dependencies
  • Clear Test Naming: Test intent at a glance
  • Precise Assertions: Verify expected specific behavior

Design Principles

  • FIRST Principle: Fast, Independent, Repeatable, Self-validating, Timely
  • AAA Pattern: Arrange-Act-Assert
  • Single Responsibility: Each test verifies one behavior
  • Readability First: Test code should be clear and understandable

Unit testing is an important means of ensuring code quality. Good tests can not only find bugs but also serve as documentation and design guidance for code.

Content is for learning and research only.