Skip to content

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,还能作为代码的文档和设计的指导。

本站内容仅供学习和研究使用。