C++ 单元测试
概述
单元测试是软件开发中的重要实践,用于验证代码的最小可测试单元是否按预期工作。C++有多种测试框架可选,本章介绍Google Test框架的使用、测试驱动开发(TDD)、模拟对象(Mock)等测试技术。
🧪 Google Test基础
环境搭建和基本用法
cpp
#include <gtest/gtest.h>
#include <string>
#include <vector>
#include <stdexcept>
// 被测试的类
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;
}
};
// 基本测试用例
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);
// 测试异常
EXPECT_THROW(calc.divide(5, 0), std::invalid_argument);
}
TEST(CalculatorTest, PrimeCheck) {
Calculator calc;
// 质数测试
EXPECT_TRUE(calc.isPrime(2));
EXPECT_TRUE(calc.isPrime(3));
EXPECT_TRUE(calc.isPrime(17));
// 非质数测试
EXPECT_FALSE(calc.isPrime(1));
EXPECT_FALSE(calc.isPrime(4));
EXPECT_FALSE(calc.isPrime(15));
}断言和期望
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;
}
};
// 各种断言类型
TEST(AssertionTest, BasicAssertions) {
// 布尔断言
EXPECT_TRUE(true);
EXPECT_FALSE(false);
// 相等断言
EXPECT_EQ(42, 42);
EXPECT_NE(42, 43);
// 比较断言
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");
// 字符串包含
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;
// 浮点数比较
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)
类级别固件
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());
}
};
// 测试固件类
class NumberListTest : public ::testing::Test {
protected:
void SetUp() override {
// 每个测试前执行
list.add(1);
list.add(3);
list.add(2);
}
void TearDown() override {
// 每个测试后执行
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);
}参数化测试
cpp
#include <gtest/gtest.h>
#include <cmath>
// 被测试函数
bool isPerfectSquare(int n) {
if (n < 0) return false;
int root = static_cast<int>(std::sqrt(n));
return root * root == n;
}
// 参数化测试
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);
}
// 测试数据
INSTANTIATE_TEST_SUITE_P(
PerfectSquareValues,
PerfectSquareTest,
::testing::Values(
std::make_pair(0, true), // 0是完全平方数
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不是完全平方数
std::make_pair(3, false), // 3不是完全平方数
std::make_pair(5, false), // 5不是完全平方数
std::make_pair(-1, false) // 负数不是完全平方数
)
);
// 类型参数化测试
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)
Google Mock基础
cpp
#include <gmock/gmock.h>
#include <gtest/gtest.h>
// 接口定义
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 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));
};
// 使用数据库的服务类
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测试
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) {
// 设置期望
EXPECT_CALL(mockDatabase, connect("test_connection"))
.WillOnce(::testing::Return(true));
// 执行测试
bool result = service->initialize("test_connection");
// 验证结果
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();
}高级Mock技术
cpp
#include <gmock/gmock.h>
// 文件系统接口
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));
};
// 配置管理器
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) {
// 使用匹配器
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));
// 不应该调用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) {
// 第一次调用返回true(文件存在),第二次返回false(文件不存在)
EXPECT_CALL(mockFileSystem, fileExists("config.txt"))
.WillOnce(::testing::Return(true))
.WillOnce(::testing::Return(false));
EXPECT_CALL(mockFileSystem, deleteFile("config.txt"))
.WillOnce(::testing::Return(true));
// 第一次重置
EXPECT_TRUE(manager->resetConfig());
// 第二次重置(文件已不存在)
EXPECT_TRUE(manager->resetConfig());
}🔄 测试驱动开发 (TDD)
TDD实践示例
cpp
#include <gtest/gtest.h>
#include <string>
#include <unordered_map>
// 购物车类的TDD开发
class ShoppingCart {
private:
std::unordered_map<std::string, std::pair<double, int>> items_; // 商品名 -> (价格, 数量)
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步骤1:红色 - 写失败的测试
TEST(ShoppingCartTest, EmptyCartInitially) {
ShoppingCart cart;
EXPECT_TRUE(cart.isEmpty());
EXPECT_EQ(cart.getItemCount(), 0);
EXPECT_DOUBLE_EQ(cart.getTotal(), 0.0);
}
// TDD步骤2:绿色 - 让测试通过
// TDD步骤3:重构 - 改进代码质量
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);
// 更新为0应该移除商品
cart.updateQuantity("Apple", 0);
EXPECT_FALSE(cart.hasItem("Apple"));
EXPECT_TRUE(cart.isEmpty());
}🚀 高级测试技术
死亡测试
cpp
#include <gtest/gtest.h>
void criticalFunction(int value) {
if (value < 0) {
abort();
}
// 正常逻辑
}
void assertFunction(bool condition) {
assert(condition);
}
// 死亡测试
TEST(DeathTest, CriticalFunctionAborts) {
EXPECT_DEATH(criticalFunction(-1), ".*");
}
TEST(DeathTest, AssertionFailure) {
EXPECT_DEATH(assertFunction(false), ".*");
}
// 在DEBUG模式下才有断言
#ifdef NDEBUG
TEST(DeathTest, AssertionInRelease) {
// 在Release模式下,断言被禁用
EXPECT_NO_FATAL_FAILURE(assertFunction(false));
}
#endif性能测试集成
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_LT(sortTime, 10000.0) << "排序耗时过长: " << sortTime << " 微秒";
// 验证确实已排序
EXPECT_TRUE(std::is_sorted(data.begin(), data.end()));
}运行和配置
cpp
// main函数示例
int main(int argc, char** argv) {
// 初始化Google Test
::testing::InitGoogleTest(&argc, argv);
// 运行所有测试
return RUN_ALL_TESTS();
}
// 测试过滤器示例
// 只运行Calculator相关测试:--gtest_filter=Calculator*
// 排除性能测试:--gtest_filter=-*Performance*
// 重复运行3次:--gtest_repeat=3
// 详细输出:--gtest_verbose总结
测试框架特性
- Google Test: 丰富的断言、固件支持
- Google Mock: 强大的模拟对象功能
- 参数化测试: 数据驱动测试
- 死亡测试: 验证程序崩溃行为
测试类型
| 测试类型 | 目的 | 工具 |
|---|---|---|
| 单元测试 | 验证单个函数/类 | Google Test |
| 集成测试 | 验证组件交互 | Mock Objects |
| 性能测试 | 验证性能指标 | 计时器 |
| 参数化测试 | 批量测试数据 | TEST_P |
最佳实践
- TDD开发流程: 红-绿-重构
- 测试隔离: 每个测试独立运行
- Mock适度使用: 只Mock必要的依赖
- 测试命名清晰: 测试意图一目了然
- 断言精确: 验证期望的具体行为
设计原则
- FIRST原则: Fast, Independent, Repeatable, Self-validating, Timely
- AAA模式: Arrange-Act-Assert
- 单一职责: 每个测试验证一个行为
- 可读性优先: 测试代码要清晰易懂
单元测试是保证代码质量的重要手段,好的测试不仅能发现Bug,还能作为代码的文档和设计的指导。