智能图书馆管理系统开发实战系列(六):Google Test单元测试实践
前言
在前面的文章中,我们完成了前后端集成,系统的核心功能已经实现。但是一个高质量的软件项目离不开完善的测试体系。本文将详细介绍如何使用Google Test框架为我们的C++后端代码编写全面的单元测试,以及如何在CI/CD流程中集成自动化测试。
Google Test框架概述
为什么选择Google Test?
Google Test(简称gtest)是Google开发的C++单元测试框架,具有以下优势:
- 功能完善: 提供丰富的断言宏和测试功能
- 易于使用: 简洁的API设计,学习成本低
- 跨平台: 支持Windows、Linux、macOS等多平台
- 集成友好: 易于与CMake、CI/CD等工具集成
- 社区活跃: 广泛使用,文档完善
测试项目架构
从 code/backend/gtester/ 目录结构可以看到我们的测试组织方式:
code/backend/gtester/
├── CMakeLists.txt # CMake测试配置
├── readme.txt # 测试说明文档
├── Src/ # 测试源代码
│ ├── main.cpp # 测试主入口
│ └── TestFramework/ # 测试框架组织
│ ├── BookManager/ # 图书管理测试
│ ├── ReaderManager/ # 读者管理测试
│ ├── LoanManager/ # 借阅管理测试
│ ├── Dashboard/ # 仪表板测试
│ ├── QuerySearch/ # 查询搜索测试
│ ├── StatisticsReport/ # 统计报表测试
│ ├── SystemSettings/ # 系统设置测试
│ ├── LibGlobal/ # 全局功能测试
│ ├── HelloWorldTest.cpp # 基础测试示例
│ └── TestBase.h # 测试基础类
测试组织特点:
- 模块对应: 测试结构与业务代码结构一一对应
- 功能细分: 每个具体功能都有独立的测试模块
- 层次清晰: 从全局测试到具体功能测试的层次结构
CMake测试配置
基础配置解析
从 code/backend/gtester/CMakeLists.txt 的配置可以看到测试项目的构建设置:
cmake_minimum_required(VERSION 3.10)
project (GTestRunner)
# Unicode配置支持
if(MSVC)
add_definitions(-DUNICODE -D_UNICODE)
# 启用UTF-8源码编码
add_compile_options(/utf-8)
# 设置字符集为Unicode
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHa")
endif()
# 非MSVC编译器配置
if(NOT MSVC)
add_definitions(-DUNICODE -D_UNICODE)
add_compile_options(-finput-charset=UTF-8 -fexec-charset=UTF-8)
endif()
# 环境变量配置
SET(UNISDK_ROOT_PROJ "$ENV{UNISDK_ROOT}")
cmake_policy(SET CMP0053 NEW)
# 构建输出优化
set(CMAKE_SUPPRESS_REGENERATION true)
set_directory_properties(PROPERTIES
ADDITIONAL_MAKE_CLEAN_FILES "${CMAKE_BINARY_DIR}/ZERO_CHECK"
)
配置亮点:
- 跨平台支持: 同时支持MSVC和GCC/Clang编译器
- Unicode支持: 完整的Unicode字符支持
- 异常处理: 配置C++异常处理机制
- 构建优化: 优化构建输出,减少无用文件
Google Test集成配置
# 查找Google Test
find_package(GTest REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
# 包含被测试的业务代码头文件
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../dll/Src)
include_directories(${UNISDK_ROOT_PROJ}/export_csdk/)
include_directories(${UNISDK_ROOT_PROJ}/export_cppsdk/)
# 组织测试源文件
file(GLOB TESTFRAMEWORK_GROUP_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/Src/TestFramework/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Src/TestFramework/*.h"
)
file(GLOB TESTFRAMEWORK_BOOKMANAGER_GROUP_FILES
"${CMAKE_CURRENT_SOURCE_DIR}/Src/TestFramework/BookManager/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Src/TestFramework/BookManager/*.h"
)
# 创建测试可执行文件
add_executable(GTestRunner
${TESTFRAMEWORK_GROUP_FILES}
${TESTFRAMEWORK_BOOKMANAGER_GROUP_FILES}
${TESTFRAMEWORK_LOANMANAGER_GROUP_FILES}
${TESTFRAMEWORK_DASHBOARD_GROUP_FILES}
)
# 链接Google Test库和业务代码库
target_link_libraries(GTestRunner
${GTEST_LIBRARIES}
${GTEST_MAIN_LIBRARIES}
libBackend # 链接我们的业务逻辑DLL
pthread # Linux下需要线程库
)
# 启用测试
enable_testing()
add_test(NAME LibrarySystemTests COMMAND GTestRunner)
核心测试模块实现
1. 图书管理模块测试
添加图书功能测试
// Src/TestFramework/BookManager/AddBook/AddBookTest.cpp
#include <gtest/gtest.h>
#include <json/json.h>
#include "Impl/BookManager/BookManager.h"
#include "TestFramework/TestHelper.h"
using namespace LibrarySystem::BookManager;
class AddBookTest : public ::testing::Test {
protected:
void SetUp() override {
// 测试前准备:初始化测试环境
TestHelper::InitializeTestDatabase();
TestHelper::ClearBookData();
}
void TearDown() override {
// 测试后清理:清理测试数据
TestHelper::ClearBookData();
TestHelper::CleanupTestDatabase();
}
// 创建有效的图书数据
Json::Value CreateValidBookData() {
Json::Value book;
book["title"] = "Test Book Title";
book["author"] = "Test Author";
book["isbn"] = "9787111213826";
book["category"] = "Technology";
book["publisher"] = "Test Publisher";
book["publishDate"] = "2024-01-01";
book["description"] = "Test Description";
return book;
}
// 创建无效的图书数据
Json::Value CreateInvalidBookData(const std::string& invalidField) {
Json::Value book = CreateValidBookData();
if (invalidField == "title") {
book["title"] = "";
} else if (invalidField == "isbn") {
book["isbn"] = "invalid-isbn";
} else if (invalidField == "missing_required") {
book.removeMember("title");
}
return book;
}
};
// 测试成功添加图书
TEST_F(AddBookTest, AddValidBook_Success) {
// Arrange
Json::Value bookData = CreateValidBookData();
std::string bookJson = TestHelper::JsonToString(bookData);
std::string resultJson;
// Act
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
// Assert
EXPECT_TRUE(result);
EXPECT_FALSE(resultJson.empty());
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
EXPECT_TRUE(resultData.isMember("bookId"));
EXPECT_FALSE(resultData["bookId"].asString().empty());
// 验证图书是否真的被添加到数据库
std::string bookId = resultData["bookId"].asString();
EXPECT_TRUE(TestHelper::IsBookExistsInDatabase(bookId));
}
// 测试添加重复ISBN的图书
TEST_F(AddBookTest, AddDuplicateISBN_Failure) {
// Arrange
Json::Value bookData = CreateValidBookData();
std::string bookJson = TestHelper::JsonToString(bookData);
std::string resultJson1, resultJson2;
// 先添加一本书
bool firstResult = BookManagerImpl::AddBook(bookJson, resultJson1);
ASSERT_TRUE(firstResult);
// Act: 尝试添加相同ISBN的书
bool secondResult = BookManagerImpl::AddBook(bookJson, resultJson2);
// Assert
EXPECT_FALSE(secondResult);
Json::Value resultData = TestHelper::StringToJson(resultJson2);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Book already exists");
}
// 测试添加无效数据的图书
TEST_F(AddBookTest, AddInvalidBook_Failure) {
// 测试空标题
{
Json::Value invalidBook = CreateInvalidBookData("title");
std::string bookJson = TestHelper::JsonToString(invalidBook);
std::string resultJson;
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Invalid book data");
}
// 测试无效ISBN
{
Json::Value invalidBook = CreateInvalidBookData("isbn");
std::string bookJson = TestHelper::JsonToString(invalidBook);
std::string resultJson;
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
}
}
// 测试JSON格式错误
TEST_F(AddBookTest, AddInvalidJSON_Failure) {
// Arrange
std::string invalidJson = "{ invalid json format }";
std::string resultJson;
// Act
bool result = BookManagerImpl::AddBook(invalidJson, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Invalid JSON format");
}
// 性能测试:批量添加图书
TEST_F(AddBookTest, BatchAddBooks_Performance) {
const int bookCount = 1000;
std::vector<std::string> bookIds;
auto startTime = std::chrono::high_resolution_clock::now();
for (int i = 0; i < bookCount; ++i) {
Json::Value bookData = CreateValidBookData();
bookData["title"] = "Test Book " + std::to_string(i);
bookData["isbn"] = TestHelper::GenerateISBN(i);
std::string bookJson = TestHelper::JsonToString(bookData);
std::string resultJson;
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
ASSERT_TRUE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
bookIds.push_back(resultData["bookId"].asString());
}
auto endTime = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime);
// 性能要求:1000本书添加应该在5秒内完成
EXPECT_LT(duration.count(), 5000);
// 验证所有图书都被正确添加
EXPECT_EQ(bookIds.size(), bookCount);
for (const auto& bookId : bookIds) {
EXPECT_TRUE(TestHelper::IsBookExistsInDatabase(bookId));
}
std::cout << "Added " << bookCount << " books in " << duration.count() << "ms" << std::endl;
}
删除图书功能测试
// Src/TestFramework/BookManager/DeleteBook/DeleteBookTest.cpp
#include <gtest/gtest.h>
#include "Impl/BookManager/BookManager.h"
#include "TestFramework/TestHelper.h"
class DeleteBookTest : public ::testing::Test {
protected:
void SetUp() override {
TestHelper::InitializeTestDatabase();
TestHelper::ClearBookData();
// 预先添加一些测试图书
testBookId = TestHelper::AddTestBook("Test Book for Deletion");
borrowedBookId = TestHelper::AddTestBook("Borrowed Book");
TestHelper::BorrowBookInTest(borrowedBookId, "test_user_001");
}
void TearDown() override {
TestHelper::ClearBookData();
TestHelper::CleanupTestDatabase();
}
std::string testBookId;
std::string borrowedBookId;
};
// 测试删除可用图书
TEST_F(DeleteBookTest, DeleteAvailableBook_Success) {
// Arrange
ASSERT_TRUE(TestHelper::IsBookExistsInDatabase(testBookId));
std::string resultJson;
// Act
bool result = BookManagerImpl::DeleteBook(testBookId, resultJson);
// Assert
EXPECT_TRUE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
// 验证图书已从数据库中删除
EXPECT_FALSE(TestHelper::IsBookExistsInDatabase(testBookId));
}
// 测试删除不存在的图书
TEST_F(DeleteBookTest, DeleteNonExistentBook_Failure) {
// Arrange
std::string nonExistentId = "non_existent_book_id";
std::string resultJson;
// Act
bool result = BookManagerImpl::DeleteBook(nonExistentId, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Book not found");
}
// 测试删除已借出的图书
TEST_F(DeleteBookTest, DeleteBorrowedBook_Failure) {
// Arrange
ASSERT_TRUE(TestHelper::IsBookBorrowed(borrowedBookId));
std::string resultJson;
// Act
bool result = BookManagerImpl::DeleteBook(borrowedBookId, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Cannot delete borrowed book");
// 验证图书仍然存在
EXPECT_TRUE(TestHelper::IsBookExistsInDatabase(borrowedBookId));
}
2. 借阅管理模块测试
// Src/TestFramework/LoanManager/LoanManagerTest.cpp
#include <gtest/gtest.h>
#include "Impl/LoanManager/LoanManager.h"
#include "TestFramework/TestHelper.h"
using namespace LibrarySystem::LoanManager;
class LoanManagerTest : public ::testing::Test {
protected:
void SetUp() override {
TestHelper::InitializeTestDatabase();
TestHelper::ClearAllData();
// 创建测试数据
availableBookId = TestHelper::AddTestBook("Available Book");
borrowedBookId = TestHelper::AddTestBook("Borrowed Book");
validUserId = TestHelper::AddTestUser("Test User", "test@example.com");
blockedUserId = TestHelper::AddTestUser("Blocked User", "blocked@example.com");
// 设置已借出的图书
TestHelper::BorrowBookInTest(borrowedBookId, validUserId);
// 设置被封禁的用户
TestHelper::BlockUserInTest(blockedUserId);
}
void TearDown() override {
TestHelper::ClearAllData();
TestHelper::CleanupTestDatabase();
}
std::string availableBookId;
std::string borrowedBookId;
std::string validUserId;
std::string blockedUserId;
};
// 测试正常借书流程
TEST_F(LoanManagerTest, BorrowAvailableBook_Success) {
// Arrange
Json::Value request;
request["bookId"] = availableBookId;
request["userId"] = validUserId;
std::string requestJson = TestHelper::JsonToString(request);
std::string resultJson;
// Act
bool result = LoanManagerImpl::BorrowBook(requestJson, resultJson);
// Assert
EXPECT_TRUE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
EXPECT_TRUE(resultData.isMember("borrowId"));
EXPECT_TRUE(resultData.isMember("dueDate"));
// 验证借阅记录已创建
std::string borrowId = resultData["borrowId"].asString();
EXPECT_TRUE(TestHelper::IsBorrowRecordExists(borrowId));
// 验证图书状态已更新为已借出
EXPECT_TRUE(TestHelper::IsBookBorrowed(availableBookId));
}
// 测试借已被借出的图书
TEST_F(LoanManagerTest, BorrowUnavailableBook_Failure) {
// Arrange
Json::Value request;
request["bookId"] = borrowedBookId;
request["userId"] = validUserId;
std::string requestJson = TestHelper::JsonToString(request);
std::string resultJson;
// Act
bool result = LoanManagerImpl::BorrowBook(requestJson, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Book not available");
}
// 测试被封禁用户借书
TEST_F(LoanManagerTest, BorrowBookWithBlockedUser_Failure) {
// Arrange
Json::Value request;
request["bookId"] = availableBookId;
request["userId"] = blockedUserId;
std::string requestJson = TestHelper::JsonToString(request);
std::string resultJson;
// Act
bool result = LoanManagerImpl::BorrowBook(requestJson, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "User borrow limit exceeded");
}
// 测试归还图书
TEST_F(LoanManagerTest, ReturnBorrowedBook_Success) {
// Arrange - 先借一本书
std::string borrowId = TestHelper::CreateBorrowRecord(availableBookId, validUserId);
Json::Value request;
request["borrowId"] = borrowId;
std::string requestJson = TestHelper::JsonToString(request);
std::string resultJson;
// Act
bool result = LoanManagerImpl::ReturnBook(requestJson, resultJson);
// Assert
EXPECT_TRUE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
// 验证借阅记录状态已更新
EXPECT_TRUE(TestHelper::IsBorrowRecordReturned(borrowId));
// 验证图书状态已恢复为可用
EXPECT_FALSE(TestHelper::IsBookBorrowed(availableBookId));
}
3. 仪表板模块测试
// Src/TestFramework/DashBoard/DashBoardTest.cpp
#include <gtest/gtest.h>
#include "Impl/DashBoard/DashBoard.h"
#include "TestFramework/TestHelper.h"
using namespace LibrarySystem::Dashboard;
class DashBoardTest : public ::testing::Test {
protected:
void SetUp() override {
TestHelper::InitializeTestDatabase();
TestHelper::ClearAllData();
// 创建测试数据:10本书,5个用户,3条借阅记录
for (int i = 0; i < 10; ++i) {
TestHelper::AddTestBook("Test Book " + std::to_string(i));
}
for (int i = 0; i < 5; ++i) {
TestHelper::AddTestUser("User " + std::to_string(i),
"user" + std::to_string(i) + "@test.com");
}
// 创建一些借阅记录
TestHelper::CreateSampleBorrowRecords();
}
void TearDown() override {
TestHelper::ClearAllData();
TestHelper::CleanupTestDatabase();
}
};
// 测试获取仪表板统计数据
TEST_F(DashBoardTest, GetDashboardStats_Success) {
// Arrange
std::string resultJson;
// Act
bool result = DashboardImpl::GetDashboardStats(resultJson);
// Assert
EXPECT_TRUE(result);
EXPECT_FALSE(resultJson.empty());
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
EXPECT_TRUE(resultData.isMember("stats"));
Json::Value stats = resultData["stats"];
// 验证统计数据
EXPECT_EQ(stats["totalBooks"].asInt(), 10);
EXPECT_EQ(stats["totalUsers"].asInt(), 5);
EXPECT_GT(stats["activeBorrows"].asInt(), 0);
EXPECT_GE(stats["availableBooks"].asInt(), 0);
// 验证图表数据
EXPECT_TRUE(resultData.isMember("borrowTrend"));
EXPECT_TRUE(resultData.isMember("categoryDistribution"));
Json::Value borrowTrend = resultData["borrowTrend"];
EXPECT_TRUE(borrowTrend.isArray());
EXPECT_GT(borrowTrend.size(), 0);
}
// 测试空数据库的仪表板统计
TEST_F(DashBoardTest, GetDashboardStatsEmptyDB_Success) {
// Arrange - 清空所有数据
TestHelper::ClearAllData();
std::string resultJson;
// Act
bool result = DashboardImpl::GetDashboardStats(resultJson);
// Assert
EXPECT_TRUE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_TRUE(resultData["success"].asBool());
Json::Value stats = resultData["stats"];
EXPECT_EQ(stats["totalBooks"].asInt(), 0);
EXPECT_EQ(stats["totalUsers"].asInt(), 0);
EXPECT_EQ(stats["activeBorrows"].asInt(), 0);
}
测试辅助工具
测试助手类实现
// Src/TestFramework/TestHelper.h
#ifndef TEST_HELPER_H
#define TEST_HELPER_H
#include <string>
#include <json/json.h>
class TestHelper {
public:
// 数据库管理
static bool InitializeTestDatabase();
static void CleanupTestDatabase();
static void ClearAllData();
static void ClearBookData();
static void ClearUserData();
static void ClearBorrowData();
// JSON工具
static std::string JsonToString(const Json::Value& json);
static Json::Value StringToJson(const std::string& jsonStr);
// 测试数据创建
static std::string AddTestBook(const std::string& title);
static std::string AddTestUser(const std::string& name, const std::string& email);
static std::string CreateBorrowRecord(const std::string& bookId, const std::string& userId);
static void CreateSampleBorrowRecords();
// 数据验证
static bool IsBookExistsInDatabase(const std::string& bookId);
static bool IsUserExistsInDatabase(const std::string& userId);
static bool IsBorrowRecordExists(const std::string& borrowId);
static bool IsBookBorrowed(const std::string& bookId);
static bool IsBorrowRecordReturned(const std::string& borrowId);
// 测试数据操作
static void BorrowBookInTest(const std::string& bookId, const std::string& userId);
static void BlockUserInTest(const std::string& userId);
static std::string GenerateISBN(int seed);
private:
static std::string GetTestDatabasePath();
static void ExecuteSQL(const std::string& sql);
};
#endif // TEST_HELPER_H
Mock和Stub实现
// Src/TestFramework/MockDatabase.h
#ifndef MOCK_DATABASE_H
#define MOCK_DATABASE_H
#include <gmock/gmock.h>
#include "Database/DatabaseManager.h"
class MockDatabase : public DatabaseManager {
public:
MOCK_METHOD(bool, Initialize, (const std::string& dbPath), (override));
MOCK_METHOD(void, Shutdown, (), (override));
MOCK_METHOD(bool, BeginTransaction, (), (override));
MOCK_METHOD(bool, CommitTransaction, (), (override));
MOCK_METHOD(bool, RollbackTransaction, (), (override));
MOCK_METHOD(bool, InsertBook, (const Json::Value& bookData), (override));
MOCK_METHOD(bool, UpdateBook, (const std::string& bookId, const Json::Value& bookData), (override));
MOCK_METHOD(bool, DeleteBook, (const std::string& bookId), (override));
MOCK_METHOD(bool, GetBookById, (const std::string& bookId, Json::Value& bookData), (override));
MOCK_METHOD(int, GetTotalBooksCount, (), (override));
MOCK_METHOD(int, GetAvailableBooksCount, (), (override));
MOCK_METHOD(Json::Value, GetBorrowTrendData, (int days), (override));
};
// 使用Mock的测试示例
class BookManagerMockTest : public ::testing::Test {
protected:
void SetUp() override {
mockDb = std::make_shared<MockDatabase>();
// 注入Mock对象到被测试类中
BookManagerImpl::SetDatabaseInstance(mockDb);
}
void TearDown() override {
BookManagerImpl::ResetDatabaseInstance();
}
std::shared_ptr<MockDatabase> mockDb;
};
TEST_F(BookManagerMockTest, AddBook_DatabaseFailure_HandledGracefully) {
// Arrange
EXPECT_CALL(*mockDb, InsertBook(::testing::_))
.WillOnce(::testing::Return(false));
Json::Value bookData;
bookData["title"] = "Test Book";
std::string bookJson = TestHelper::JsonToString(bookData);
std::string resultJson;
// Act
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
// Assert
EXPECT_FALSE(result);
Json::Value resultData = TestHelper::StringToJson(resultJson);
EXPECT_FALSE(resultData["success"].asBool());
EXPECT_EQ(resultData["error"].asString(), "Database operation failed");
}
测试覆盖率与质量
代码覆盖率配置
# CMakeLists.txt 中添加覆盖率支持
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
if(CMAKE_COMPILER_IS_GNUCXX)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
endif()
# 添加覆盖率目标
if(ENABLE_COVERAGE)
find_program(LCOV_PATH lcov)
find_program(GENHTML_PATH genhtml)
if(LCOV_PATH AND GENHTML_PATH)
add_custom_target(coverage
COMMAND ${LCOV_PATH} --directory . --capture --output-file coverage.info
COMMAND ${LCOV_PATH} --remove coverage.info '/usr/*' --output-file coverage.info
COMMAND ${LCOV_PATH} --list coverage.info
COMMAND ${GENHTML_PATH} -o coverage coverage.info
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
endif()
endif()
性能测试基准
// Src/TestFramework/PerformanceTest.cpp
#include <gtest/gtest.h>
#include <benchmark/benchmark.h>
#include "Impl/BookManager/BookManager.h"
#include "TestFramework/TestHelper.h"
// 图书搜索性能测试
static void BM_BookSearch(benchmark::State& state) {
TestHelper::InitializeTestDatabase();
// 预先插入大量测试数据
for (int i = 0; i < 10000; ++i) {
TestHelper::AddTestBook("Performance Test Book " + std::to_string(i));
}
Json::Value query;
query["keyword"] = "Performance";
query["page"] = 1;
query["pageSize"] = 50;
std::string queryJson = TestHelper::JsonToString(query);
for (auto _ : state) {
std::string resultJson;
BookManagerImpl::GetBookList(queryJson, resultJson);
}
TestHelper::CleanupTestDatabase();
}
BENCHMARK(BM_BookSearch);
// 批量操作性能测试
static void BM_BatchAddBooks(benchmark::State& state) {
const int batchSize = state.range(0);
for (auto _ : state) {
state.PauseTiming();
TestHelper::InitializeTestDatabase();
state.ResumeTiming();
for (int i = 0; i < batchSize; ++i) {
Json::Value bookData;
bookData["title"] = "Batch Book " + std::to_string(i);
bookData["author"] = "Batch Author";
bookData["isbn"] = TestHelper::GenerateISBN(i);
bookData["category"] = "Test";
bookData["publisher"] = "Test Publisher";
bookData["publishDate"] = "2024-01-01";
std::string bookJson = TestHelper::JsonToString(bookData);
std::string resultJson;
BookManagerImpl::AddBook(bookJson, resultJson);
}
state.PauseTiming();
TestHelper::CleanupTestDatabase();
state.ResumeTiming();
}
}
BENCHMARK(BM_BatchAddBooks)->Range(100, 1000);
// 运行基准测试
BENCHMARK_MAIN();
CI/CD集成
GitHub Actions配置
# .github/workflows/test.yml
name: C++ Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
build_type: [Debug, Release]
steps:
- uses: actions/checkout@v3
- name: Install dependencies (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y cmake build-essential libgtest-dev libgmock-dev
sudo apt-get install -y lcov
- name: Install dependencies (Windows)
if: matrix.os == 'windows-latest'
run: |
vcpkg install gtest gmock sqlite3
- name: Configure CMake
run: |
cmake -B ${{github.workspace}}/build
-DCMAKE_BUILD_TYPE=${{matrix.build_type}}
-DENABLE_TESTING=ON
-DENABLE_COVERAGE=ON
- name: Build
run: cmake --build ${{github.workspace}}/build --config ${{matrix.build_type}}
- name: Test
working-directory: ${{github.workspace}}/build
run: ctest -C ${{matrix.build_type}} --output-on-failure
- name: Generate Coverage Report (Ubuntu)
if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Debug'
working-directory: ${{github.workspace}}/build
run: make coverage
- name: Upload Coverage to Codecov
if: matrix.os == 'ubuntu-latest' && matrix.build_type == 'Debug'
uses: codecov/codecov-action@v3
with:
file: ${{github.workspace}}/build/coverage.info
fail_ci_if_error: true
本地测试脚本
#!/bin/bash
# scripts/run_tests.sh
set -e
# 配置参数
BUILD_TYPE=${BUILD_TYPE:-Debug}
ENABLE_COVERAGE=${ENABLE_COVERAGE:-ON}
TEST_FILTER=${TEST_FILTER:-"*"}
echo "Building and running tests..."
echo "Build Type: $BUILD_TYPE"
echo "Coverage: $ENABLE_COVERAGE"
echo "Test Filter: $TEST_FILTER"
# 创建构建目录
mkdir -p build
cd build
# 配置CMake
cmake .. \
-DCMAKE_BUILD_TYPE=$BUILD_TYPE \
-DENABLE_TESTING=ON \
-DENABLE_COVERAGE=$ENABLE_COVERAGE
# 构建项目
cmake --build . --config $BUILD_TYPE -j$(nproc)
# 运行测试
echo "Running unit tests..."
./code/backend/gtester/GTestRunner --gtest_filter="$TEST_FILTER" --gtest_output=xml:test_results.xml
# 生成覆盖率报告
if [ "$ENABLE_COVERAGE" = "ON" ]; then
echo "Generating coverage report..."
make coverage
echo "Coverage report generated in build/coverage/"
fi
echo "Tests completed successfully!"
Windows批处理脚本
@echo off
REM scripts/run_tests.bat
setlocal EnableDelayedExpansion
set BUILD_TYPE=Debug
if not "%1"=="" set BUILD_TYPE=%1
set ENABLE_COVERAGE=OFF
if not "%2"=="" set ENABLE_COVERAGE=%2
echo Building and running tests...
echo Build Type: %BUILD_TYPE%
echo Coverage: %ENABLE_COVERAGE%
REM 创建构建目录
if not exist build mkdir build
cd build
REM 配置CMake
cmake .. ^
-DCMAKE_BUILD_TYPE=%BUILD_TYPE% ^
-DENABLE_TESTING=ON ^
-DENABLE_COVERAGE=%ENABLE_COVERAGE%
if errorlevel 1 (
echo CMake configuration failed!
exit /b 1
)
REM 构建项目
cmake --build . --config %BUILD_TYPE%
if errorlevel 1 (
echo Build failed!
exit /b 1
)
REM 运行测试
echo Running unit tests...
code\backend\gtester\%BUILD_TYPE%\GTestRunner.exe --gtest_output=xml:test_results.xml
if errorlevel 1 (
echo Tests failed!
exit /b 1
)
echo Tests completed successfully!
测试最佳实践
1. 测试命名规范
// 测试命名模式:方法名_输入条件_期望结果
TEST_F(BookManagerTest, AddBook_ValidData_Success)
TEST_F(BookManagerTest, AddBook_DuplicateISBN_Failure)
TEST_F(BookManagerTest, AddBook_InvalidJSON_Failure)
TEST_F(BookManagerTest, DeleteBook_BorrowedBook_Failure)
2. 测试数据管理
class TestDataManager {
public:
// 测试数据构建器模式
class BookBuilder {
public:
BookBuilder& withTitle(const std::string& title) {
data["title"] = title;
return *this;
}
BookBuilder& withAuthor(const std::string& author) {
data["author"] = author;
return *this;
}
BookBuilder& withISBN(const std::string& isbn) {
data["isbn"] = isbn;
return *this;
}
Json::Value build() const {
return data;
}
private:
Json::Value data;
};
static BookBuilder createBook() {
return BookBuilder()
.withTitle("Default Title")
.withAuthor("Default Author")
.withISBN("9787111213826");
}
};
// 使用示例
TEST_F(BookManagerTest, AddBook_CustomData_Success) {
auto book = TestDataManager::createBook()
.withTitle("Custom Book Title")
.withAuthor("Custom Author")
.build();
std::string bookJson = TestHelper::JsonToString(book);
std::string resultJson;
bool result = BookManagerImpl::AddBook(bookJson, resultJson);
EXPECT_TRUE(result);
}
3. 异步测试处理
// 异步操作测试
TEST_F(BookManagerTest, AsyncBookSearch_LargeDataset_CompletesInTime) {
const int timeout_ms = 5000;
std::promise<bool> promise;
std::future<bool> future = promise.get_future();
// 启动异步搜索
std::thread searchThread([&]() {
try {
Json::Value query;
query["keyword"] = "test";
std::string queryJson = TestHelper::JsonToString(query);
std::string resultJson;
bool result = BookManagerImpl::GetBookList(queryJson, resultJson);
promise.set_value(result);
} catch (...) {
promise.set_value(false);
}
});
// 等待结果或超时
auto status = future.wait_for(std::chrono::milliseconds(timeout_ms));
ASSERT_EQ(status, std::future_status::ready);
EXPECT_TRUE(future.get());
if (searchThread.joinable()) {
searchThread.join();
}
}
下期预告
在下一篇文章中,我们将介绍CMake构建系统的高级用法与持续集成实践,包括如何设置复杂的构建配置、依赖管理,以及在不同平台上的部署策略。
总结
本文详细介绍了Google Test单元测试的完整实践,包括:
- 测试框架搭建: 基于CMake的测试项目组织
- 模块化测试: 按业务模块组织的测试结构
- 全面测试覆盖: 正常流程、异常处理、边界条件测试
- 测试工具: Mock、Stub、测试助手类的使用
- 性能测试: 基准测试和性能监控
- CI/CD集成: 自动化测试和持续集成流程
- 最佳实践: 测试命名、数据管理、异步测试
通过完善的测试体系,我们确保了C++后端代码的质量和稳定性,为整个智能图书馆管理系统提供了可靠的质量保障。
系列文章目录
- 项目架构设计与技术选型
- 高保真原型设计与用户体验测试
- 前端工程化实践:Electron + React + TypeScript
- 后端C++ DLL开发与模块化设计
- 前后端集成:koffi调用与接口设计
- Google Test单元测试实践
- CMake构建系统与持续集成
- 性能优化与部署发布
通过这个系列文章,您将学习到现代桌面应用开发的完整流程和最佳实践。

被折叠的 条评论
为什么被折叠?



