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));
}
#endifPerformance 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_verboseSummary
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 Type | Purpose | Tools |
|---|---|---|
| Unit Testing | Verify single function/class | Google Test |
| Integration Testing | Verify component interaction | Mock Objects |
| Performance Testing | Verify performance metrics | Timers |
| Parameterized Testing | Batch test data | TEST_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.