智能图书馆管理系统开发实战系列(六):Google Test单元测试实践

智能图书馆管理系统开发实战系列(六):Google Test单元测试实践

前言

在前面的文章中,我们完成了前后端集成,系统的核心功能已经实现。但是一个高质量的软件项目离不开完善的测试体系。本文将详细介绍如何使用Google Test框架为我们的C++后端代码编写全面的单元测试,以及如何在CI/CD流程中集成自动化测试。

Google Test框架概述

为什么选择Google Test?

Google Test(简称gtest)是Google开发的C++单元测试框架,具有以下优势:

  1. 功能完善: 提供丰富的断言宏和测试功能
  2. 易于使用: 简洁的API设计,学习成本低
  3. 跨平台: 支持Windows、Linux、macOS等多平台
  4. 集成友好: 易于与CMake、CI/CD等工具集成
  5. 社区活跃: 广泛使用,文档完善

测试项目架构

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单元测试的完整实践,包括:

  1. 测试框架搭建: 基于CMake的测试项目组织
  2. 模块化测试: 按业务模块组织的测试结构
  3. 全面测试覆盖: 正常流程、异常处理、边界条件测试
  4. 测试工具: Mock、Stub、测试助手类的使用
  5. 性能测试: 基准测试和性能监控
  6. CI/CD集成: 自动化测试和持续集成流程
  7. 最佳实践: 测试命名、数据管理、异步测试

通过完善的测试体系,我们确保了C++后端代码的质量和稳定性,为整个智能图书馆管理系统提供了可靠的质量保障。

系列文章目录

  1. 项目架构设计与技术选型
  2. 高保真原型设计与用户体验测试
  3. 前端工程化实践:Electron + React + TypeScript
  4. 后端C++ DLL开发与模块化设计
  5. 前后端集成:koffi调用与接口设计
  6. Google Test单元测试实践
  7. CMake构建系统与持续集成
  8. 性能优化与部署发布

通过这个系列文章,您将学习到现代桌面应用开发的完整流程和最佳实践。

程序及源码附件下载

程序及源码

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值