1. 项目概述:为什么选择C++与Selenium的组合?

在金融行业,系统的稳定性、安全性和性能是生命线。一个交易指令的延迟、一个数据计算的错误,都可能带来难以估量的损失。因此,金融系统的测试,尤其是回归测试,其频率和严谨性远超普通应用。传统的纯手工测试在面对频繁迭代的金融产品时,不仅效率低下,而且极易因人为疲劳导致漏测。自动化测试是必然选择。

提到UI自动化测试,大家第一时间想到的可能是Python + Selenium的组合,这确实是入门最快、生态最丰富的方案。但在高性能、高并发的金融核心系统中,比如量化交易平台、风险控制系统,其后台引擎和部分关键客户端往往由C++编写,以求极致性能。此时,如果自动化测试脚本也用Python来写,在驱动这些C++应用或与底层C++服务交互时,往往会遇到性能瓶颈或复杂的进程间通信问题。更直接的需求是:我们需要测试一个用C++编写的、带有图形界面的交易客户端。

这就是本项目的核心价值所在: 直接使用C++语言调用Selenium WebDriver,来驱动浏览器,完成对金融Web系统的自动化测试 。这听起来有点“跨界”,但逻辑很清晰:金融系统的前端(如交易员门户、风险管理看板)越来越多地采用Web技术(React, Vue.js)构建,以保证跨平台和快速迭代。而测试这些前端的自动化脚本,如果由开发核心引擎的C++团队来编写和维护,可以实现技术栈统一,减少上下文切换成本,并能更紧密地与底层业务逻辑(如订单簿、风险算法)的测试相结合,构建从UI到API再到核心计算的端到端自动化测试流水线。

我最初决定走这条路,是因为我们团队维护着一个C++高频交易模拟器,其配套的风险监控面板是Web端。每次引擎逻辑更新,都需要确保监控面板的数据展示和告警功能正常。用Python写脚本总觉得隔了一层,特别是在集成到以CMake和CI/CD为核心的C++构建流程中时,环境依赖和调用链路变得复杂。最终,我们成功实现了用C++测试套件直接控制浏览器,验证前端行为,并将结果无缝纳入同一份测试报告。这套方案运行一年多,非常稳定。

2. 环境搭建与工具链选型

用C++操作Selenium,核心在于使用Selenium官方提供的WebDriver协议。浏览器(如Chrome)通过一个本地服务(ChromeDriver)暴露出一个HTTP服务器。我们的测试脚本(即WebDriver Client)通过发送HTTP请求(遵循W3C WebDriver标准)到这个服务器,来控制浏览器。因此,无论客户端用什么语言,只要能发送HTTP请求、解析JSON响应即可。

2.1 核心组件清单

  1. 浏览器 :Google Chrome(稳定版)。金融系统通常对浏览器版本有明确要求,需与生产环境一致。

  2. 浏览器驱动 :ChromeDriver。版本必须与已安装的Chrome浏览器主版本号完全匹配,否则无法启动。

  3. C++ HTTP客户端库 :这是关键。我们需要一个库来方便地发送HTTP请求(POST, GET, DELETE)并处理JSON。常见选择有:

    • cpp-httplib :一个单头文件库,简单易用,零依赖。对于WebDriver这种简单的HTTP交互足够。
    • libcurl :功能强大的老牌库,支持多种协议,但需要链接库,配置稍复杂。
    • Boost.Beast :功能强大,是编写HTTP客户端和服务器的好选择,但学习曲线较陡,且依赖整个Boost库。 我的选择是cpp-httplib 。理由很简单:它只需要一个 .hpp 文件,直接包含进项目即可,避免了在复杂的C++构建环境中管理额外的库依赖,特别适合快速集成到现有项目中。
  4. C++ JSON库 :用于构造发送给WebDriver的指令(JSON格式),并解析返回的响应。常见选择:

    • nlohmann/json :目前C++社区事实上的标准,API极其直观,像操作 std::map std::vector 一样操作JSON。
    • RapidJSON :性能极高,但API相对繁琐。 我选择nlohmann/json 。开发效率优先,其直观的语法能让我们更专注于测试逻辑本身。
  5. 构建系统 :CMake。这是现代C++项目的标配,能方便地管理依赖(虽然我们的两个主要依赖是单头文件)和构建过程。

2.2 详细安装与配置步骤

2.2.1 安装Chrome与ChromeDriver

首先,确保系统已安装Chrome。然后,按以下步骤操作:

  1. 查看Chrome版本 :打开Chrome,在地址栏输入 chrome://version/ ,查看第一行“Google Chrome”后的版本号,例如 120.0.6099.130
  2. 下载匹配的ChromeDriver :访问 ChromeDriver下载站 ,找到与你的Chrome主版本号(此例中为120)完全一致的驱动版本进行下载。
  3. 放置与配置 :将下载的 chromedriver (Windows下为 chromedriver.exe )可执行文件放在一个目录下,例如 /usr/local/bin/ (Linux/macOS)或 C:\WebDriver\bin (Windows)。 关键一步:将该目录添加到系统的PATH环境变量中 。这样,我们的C++程序就能在任意位置启动它。

注意 :在CI/CD服务器(如Jenkins, GitLab Runner)上部署时,同样需要完成上述步骤。建议将Chrome和ChromeDriver的安装与PATH配置编写成脚本,作为CI流水线的一个环节。

2.2.2 创建C++项目并集成依赖

假设我们的项目名为 FinAutoTest

  1. 项目结构

    FinAutoTest/
    ├── CMakeLists.txt
    ├── include/
    │   ├── httplib.h
    │   └── json.hpp
    ├── src/
    │   └── main.cpp
    └── tests/
    
  2. 获取依赖

  3. 编写CMakeLists.txt

    cmake_minimum_required(VERSION 3.10)
    project(FinAutoTest)
    
    set(CMAKE_CXX_STANDARD 17)
    
    # 包含头文件目录
    include_directories(${PROJECT_SOURCE_DIR}/include)
    
    # 添加可执行文件
    add_executable(fin_auto_test src/main.cpp)
    
    # 在Linux/macOS下,cpp-httplib需要链接pthread和ssl/crypto库
    if(UNIX AND NOT APPLE)
        target_link_libraries(fin_auto_test pthread ssl crypto)
    endif()
    
2.2.3 验证环境

编写一个最简单的C++程序,测试HTTP库和JSON库是否能正常工作。

// src/test_env.cpp
#include <iostream>
#include "httplib.h"
#include "json.hpp"

using json = nlohmann::json;

int main() {
    // 测试json库
    json j;
    j["test"] = "environment";
    j["status"] = "ok";
    std::cout << "JSON test: " << j.dump(2) << std::endl;

    // 测试httplib(不实际发起请求)
    httplib::Client cli("http://localhost:8080");
    std::cout << "HTTP lib initialized." << std::endl;
    return 0;
}

编译并运行,如果能看到JSON输出和初始化信息,说明基础环境OK。

3. 封装Selenium WebDriver C++客户端

直接在每个测试用例里用 httplib 拼写WebDriver协议请求是非常低效且容易出错的。我们必须进行封装,创建一个简单的 WebDriverClient 类。这个类将常用的操作(如启动会话、查找元素、点击、输入)封装成成员函数。

3.1 WebDriverClient类设计

// include/web_driver_client.h
#ifndef WEB_DRIVER_CLIENT_H
#define WEB_DRIVER_CLIENT_H

#include <string>
#include <memory>
#include "httplib.h"
#include "json.hpp"

using json = nlohmann::json;

class WebDriverClient {
public:
    // 构造函数,传入WebDriver服务地址,默认是ChromeDriver的默认地址
    WebDriverClient(const std::string& server_url = "http://localhost:9515");

    // 析构函数,确保测试结束关闭浏览器
    ~WebDriverClient();

    // 启动一个新的浏览器会话
    bool startSession(const json& desired_capabilities = {{"browserName", "chrome"}});
    
    // 导航到指定URL
    bool navigateTo(const std::string& url);
    
    // 通过CSS选择器查找单个元素
    std::string findElement(const std::string& css_selector);
    
    // 向指定元素输入文本
    bool sendKeysToElement(const std::string& element_id, const std::string& text);
    
    // 点击指定元素
    bool clickElement(const std::string& element_id);
    
    // 获取元素的文本内容
    std::string getElementText(const std::string& element_id);
    
    // 执行JavaScript脚本
    json executeScript(const std::string& script, const json& args = json::array());
    
    // 关闭当前会话和浏览器
    void quit();

    // 获取最后一次操作的错误信息(用于调试)
    std::string getLastError() const;

private:
    std::string server_url_;
    std::string session_id_; // WebDriver会话ID
    std::unique_ptr<httplib::Client> client_;
    std::string last_error_;
    
    // 内部方法:发送WebDriver命令并处理响应
    json sendCommand(const std::string& method, 
                     const std::string& endpoint, 
                     const json& data = json::object());
};

#endif // WEB_DRIVER_CLIENT_H

3.2 核心实现解析

我们来看最关键的 sendCommand 方法和 startSession 的实现。

// src/web_driver_client.cpp
#include "web_driver_client.h"
#include <iostream>

WebDriverClient::WebDriverClient(const std::string& server_url) 
    : server_url_(server_url), client_(std::make_unique<httplib::Client>(server_url_)) {
    // 设置一些合理的超时,金融系统操作可能涉及复杂页面加载
    client_->set_connection_timeout(30);
    client_->set_read_timeout(60);
    client_->set_write_timeout(30);
}

WebDriverClient::~WebDriverClient() {
    if (!session_id_.empty()) {
        quit(); // 自动清理会话
    }
}

json WebDriverClient::sendCommand(const std::string& method,
                                  const std::string& endpoint, 
                                  const json& data) {
    last_error_.clear();
    httplib::Result res;
    std::string full_path = session_id_.empty() ? endpoint : "/session/" + session_id_ + endpoint;
    
    if (method == "POST") {
        res = client_->Post(full_path.c_str(), data.dump(), "application/json");
    } else if (method == "GET") {
        res = client_->Get(full_path.c_str());
    } else if (method == "DELETE") {
        res = client_->Delete(full_path.c_str());
    } else {
        last_error_ = "Unsupported HTTP method: " + method;
        return json::object();
    }

    if (!res) {
        last_error_ = "HTTP request failed or timed out.";
        return json::object();
    }

    if (res->status >= 400) {
        // WebDriver错误,响应体中是JSON格式的错误信息
        last_error_ = "WebDriver error: HTTP " + std::to_string(res->status);
        try {
            auto error_json = json::parse(res->body);
            if (error_json.contains("value") && error_json["value"].contains("message")) {
                last_error_ += " - " + error_json["value"]["message"].get<std::string>();
            }
        } catch (...) {
            last_error_ += " - " + res->body;
        }
        return json::object();
    }

    try {
        return json::parse(res->body);
    } catch (const json::parse_error& e) {
        last_error_ = std::string("JSON parse error: ") + e.what();
        return json::object();
    }
}

bool WebDriverClient::startSession(const json& desired_capabilities) {
    json request_body = {{"capabilities", {{"alwaysMatch", desired_capabilities}}}};
    
    auto response = sendCommand("POST", "/session", request_body);
    
    if (response.empty() || !response.contains("value") || !response["value"].contains("sessionId")) {
        std::cerr << "Failed to start session. Error: " << last_error_ << std::endl;
        return false;
    }
    
    session_id_ = response["value"]["sessionId"].get<std::string>();
    std::cout << "Session started: " << session_id_ << std::endl;
    return true;
}

bool WebDriverClient::navigateTo(const std::string& url) {
    json args = {{"url", url}};
    auto response = sendCommand("POST", "/url", args);
    return !response.empty();
}

std::string WebDriverClient::findElement(const std::string& css_selector) {
    json args = {{"using", "css selector"}, {"value", css_selector}};
    auto response = sendCommand("POST", "/element", args);
    
    if (response.empty() || !response["value"].contains("element-6066-11e4-a52e-4f735466cecf")) {
        return "";
    }
    return response["value"]["element-6066-11e4-a52e-4f735466cecf"].get<std::string>();
}

bool WebDriverClient::sendKeysToElement(const std::string& element_id, const std::string& text) {
    json args = {{"text", text}};
    auto response = sendCommand("POST", "/element/" + element_id + "/value", args);
    return !response.empty();
}

bool WebDriverClient::clickElement(const std::string& element_id) {
    auto response = sendCommand("POST", "/element/" + element_id + "/click", json::object());
    return !response.empty();
}

void WebDriverClient::quit() {
    if (!session_id_.empty()) {
        sendCommand("DELETE", "");
        session_id_.clear();
        std::cout << "Session terminated." << std::endl;
    }
}

实操心得 :在实现 sendCommand 时,错误处理至关重要。WebDriver协议的错误信息藏在响应体的JSON里,必须解析出来,否则排查问题如同盲人摸象。我将错误信息存储在 last_error_ 中,并提供了 getLastError() 方法,这在调试复杂的测试用例时能救命。

4. 实战:金融系统登录与余额查询测试案例

假设我们要测试一个简易的网上银行系统,核心测试场景是:用户登录,然后检查账户概览页的余额显示是否正确。

4.1 测试页面分析与定位策略

首先,我们需要分析被测页面的HTML结构。假设登录页面如下:

<!-- 简化的登录页面 -->
<input type="text" id="username" placeholder="客户号/用户名">
<input type="password" id="password" placeholder="登录密码">
<button id="login-btn" class="btn-primary">登录</button>

账户概览页面:

<div class="account-summary">
    <h3>账户概览</h3>
    <div class="balance">
        <span class="label">可用余额:</span>
        <span id="available-balance" class="amount">1,234,567.89</span>
        <span class="currency">CNY</span>
    </div>
</div>

定位策略 :优先使用 id 选择器,因为它在整个页面中唯一,定位最准确、最快。其次是CSS选择器。应避免使用不稳定的XPath,特别是包含索引(如 div[3]/span[2] )或长路径的XPath,它们在页面结构微调时极易失效。

4.2 C++测试用例实现

我们将使用上面封装的 WebDriverClient 类来编写测试。

// src/test_banking_login.cpp
#include <iostream>
#include <cassert>
#include "web_driver_client.h"

// 一个简单的测试夹具
class BankingLoginTest {
public:
    BankingLoginTest() : driver() {}
    
    void SetUp() {
        std::cout << "Setting up test..." << std::endl;
        // 可以在这里配置Capabilities,例如无头模式、窗口大小、禁用GPU等
        json caps;
        caps["browserName"] = "chrome";
        // 无头模式,适合在CI服务器上运行,不显示UI
        // caps["goog:chromeOptions"]["args"].push_back("--headless");
        caps["goog:chromeOptions"]["args"].push_back("--disable-gpu");
        caps["goog:chromeOptions"]["args"].push_back("--no-sandbox"); // Linux CI环境常需要
        caps["goog:chromeOptions"]["args"].push_back("--window-size=1920,1080");
        
        if (!driver.startSession(caps)) {
            throw std::runtime_error("Failed to start WebDriver session: " + driver.getLastError());
        }
    }
    
    void TearDown() {
        std::cout << "Tearing down test..." << std::endl;
        driver.quit();
    }
    
    WebDriverClient driver;
};

// 测试用例:成功登录并验证余额
void test_successful_login_and_balance_check() {
    BankingLoginTest fixture;
    fixture.SetUp();
    
    try {
        // 1. 导航到登录页面
        std::string login_url = "https://demo-bank.example.com/login"; // 替换为实际地址
        if (!fixture.driver.navigateTo(login_url)) {
            throw std::runtime_error("Navigate failed: " + fixture.driver.getLastError());
        }
        // 简单等待,生产环境应使用显式等待(后面会讲)
        std::this_thread::sleep_for(std::chrono::seconds(2));
        
        // 2. 输入用户名和密码
        std::string username_field_id = fixture.driver.findElement("#username");
        if (username_field_id.empty()) {
            throw std::runtime_error("Cannot find username field.");
        }
        fixture.driver.sendKeysToElement(username_field_id, "test_user_001");
        
        std::string password_field_id = fixture.driver.findElement("#password");
        if (password_field_id.empty()) {
            throw std::runtime_error("Cannot find password field.");
        }
        fixture.driver.sendKeysToElement(password_field_id, "securePass123!");
        
        // 3. 点击登录按钮
        std::string login_btn_id = fixture.driver.findElement("#login-btn");
        if (login_btn_id.empty()) {
            throw std::runtime_error("Cannot find login button.");
        }
        fixture.driver.clickElement(login_btn_id);
        
        // 4. 等待页面跳转到概览页
        std::this_thread::sleep_for(std::chrono::seconds(3));
        
        // 5. 定位余额元素并获取文本
        std::string balance_element_id = fixture.driver.findElement("#available-balance");
        if (balance_element_id.empty()) {
            throw std::runtime_error("Cannot find balance element.");
        }
        std::string balance_text = fixture.driver.getElementText(balance_element_id);
        
        // 6. 断言:余额应为 "1,234,567.89"
        std::string expected_balance = "1,234,567.89";
        if (balance_text != expected_balance) {
            std::string error_msg = "Balance mismatch! Expected: '" + expected_balance + 
                                    "', Actual: '" + balance_text + "'";
            throw std::runtime_error(error_msg);
        }
        
        std::cout << "[PASS] Login and balance check successful. Balance: " << balance_text << std::endl;
        
    } catch (const std::exception& e) {
        std::cerr << "[FAIL] Test failed: " << e.what() << std::endl;
        std::cerr << "Last WebDriver error: " << fixture.driver.getLastError() << std::endl;
        // 可以在这里截屏,保存页面源码,用于后续分析
        fixture.TearDown();
        throw; // 重新抛出,让测试框架捕获
    }
    
    fixture.TearDown();
}

int main() {
    try {
        test_successful_login_and_balance_check();
        std::cout << "\nAll tests passed!" << std::endl;
        return 0;
    } catch (...) {
        std::cout << "\nTest run failed." << std::endl;
        return 1;
    }
}

注意事项 :上面的代码使用了 std::this_thread::sleep_for 进行等待,这是 极不推荐 的做法。在网络波动或服务器响应慢时,固定的等待时间要么浪费(等太久),要么导致失败(等不够)。 必须使用显式等待

5. 高级技巧:显式等待、页面对象模型与数据驱动

要让C++ Selenium测试稳定、可维护,必须引入这些在Python/Java Selenium中成熟的设计模式。

5.1 实现显式等待(Explicit Wait)

显式等待是等待某个条件成立后,才继续执行后续操作。我们实现一个通用的等待函数。

// include/web_driver_client.h 添加声明
bool waitForElementVisible(const std::string& css_selector, int timeout_seconds = 10);
std::string waitForAndFindElement(const std::string& css_selector, int timeout_seconds = 10);

// src/web_driver_client.cpp 添加实现
bool WebDriverClient::waitForElementVisible(const std::string& css_selector, int timeout_seconds) {
    auto start = std::chrono::steady_clock::now();
    while (std::chrono::steady_clock::now() - start < std::chrono::seconds(timeout_seconds)) {
        // 通过JavaScript检查元素是否存在且可见
        std::string script = R"(
            var el = document.querySelector(arguments[0]);
            if (el && el.offsetParent !== null) { // offsetParent不为null通常表示元素可见
                return true;
            }
            return false;
        )";
        json args = {css_selector};
        json result = executeScript(script, args);
        if (!result.empty() && result["value"].is_boolean() && result["value"].get<bool>()) {
            return true;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 轮询间隔
    }
    last_error_ = "Timeout waiting for element to be visible: " + css_selector;
    return false;
}

std::string WebDriverClient::waitForAndFindElement(const std::string& css_selector, int timeout_seconds) {
    if (waitForElementVisible(css_selector, timeout_seconds)) {
        return findElement(css_selector);
    }
    return "";
}

这样,测试用例中的查找就可以改为:

std::string login_btn_id = driver.waitForAndFindElement("#login-btn", 10);
if (login_btn_id.empty()) {
    // 处理等待超时
}

5.2 引入页面对象模型(Page Object Model, POM)

POM将每个页面封装成一个类,页面的元素定位和操作作为类的方法。这极大提升了代码的可读性和可维护性。

// include/pages/login_page.h
#ifndef LOGIN_PAGE_H
#define LOGIN_PAGE_H

#include "web_driver_client.h"
#include <string>

class LoginPage {
public:
    LoginPage(WebDriverClient& driver) : driver_(driver) {}
    
    void navigate() {
        driver_.navigateTo("https://demo-bank.example.com/login");
    }
    
    void enterUsername(const std::string& username) {
        auto elem_id = driver_.waitForAndFindElement("#username", 10);
        driver_.sendKeysToElement(elem_id, username);
    }
    
    void enterPassword(const std::string& password) {
        auto elem_id = driver_.waitForAndFindElement("#password", 10);
        driver_.sendKeysToElement(elem_id, password);
    }
    
    void clickLogin() {
        auto elem_id = driver_.waitForAndFindElement("#login-btn", 10);
        driver_.clickElement(elem_id);
    }
    
    // 可以添加更多方法,如“记住我”勾选、错误信息获取等
private:
    WebDriverClient& driver_;
};

// include/pages/overview_page.h
class OverviewPage {
public:
    OverviewPage(WebDriverClient& driver) : driver_(driver) {}
    
    std::string getAvailableBalance() {
        auto elem_id = driver_.waitForAndFindElement("#available-balance", 15); // 余额加载可能稍慢
        if (elem_id.empty()) {
            return "";
        }
        return driver_.getElementText(elem_id);
    }
    
    bool isPageLoaded() {
        // 通过检查页面特定元素来判断是否加载完成
        auto elem_id = driver_.waitForAndFindElement(".account-summary h3", 10);
        return !elem_id.empty();
    }
private:
    WebDriverClient& driver_;
};

测试用例将变得非常清晰:

void test_login_with_pom() {
    WebDriverClient driver;
    // ... 启动会话
    LoginPage login_page(driver);
    OverviewPage overview_page(driver);
    
    login_page.navigate();
    login_page.enterUsername("test_user_001");
    login_page.enterPassword("securePass123!");
    login_page.clickLogin();
    
    // 使用页面对象的方法等待页面加载
    if (!overview_page.isPageLoaded()) {
        throw std::runtime_error("Overview page failed to load.");
    }
    
    std::string balance = overview_page.getAvailableBalance();
    assert(balance == "1,234,567.89");
}

5.3 数据驱动测试

将测试数据与测试逻辑分离。我们可以从CSV、JSON或数据库中读取测试用例。

// 简单的JSON数据驱动示例
#include "json.hpp"
#include <fstream>

struct TestAccount {
    std::string username;
    std::string password;
    std::string expected_balance;
};

std::vector<TestAccount> load_test_data(const std::string& filepath) {
    std::ifstream file(filepath);
    json j;
    file >> j;
    
    std::vector<TestAccount> data;
    for (const auto& item : j) {
        data.push_back({
            item["username"].get<std::string>(),
            item["password"].get<std::string>(),
            item["expected_balance"].get<std::string>()
        });
    }
    return data;
}

void run_data_driven_test() {
    auto test_cases = load_test_data("test_data/accounts.json");
    for (const auto& tc : test_cases) {
        // 为每个测试用例创建新的driver会话,保证隔离性
        WebDriverClient driver;
        // ... 启动会话,使用tc.username, tc.password等执行测试
        // ... 断言 tc.expected_balance
        driver.quit(); // 确保每个用例结束后清理
    }
}

6. 集成到CI/CD与测试报告

自动化测试只有集成到持续集成/持续部署流水线中,才能最大化其价值。

6.1 在CMake中集成Google Test

大型项目通常会使用测试框架,如Google Test。

  1. 集成GTest :使用CMake的 FetchContent find_package 引入Google Test。
  2. 编写GTest用例 :将之前的测试用例改写成GTest的 TEST_F 格式。
  3. 编译与运行 :在CMake中定义测试目标,并配置CI脚本在构建后自动运行测试。

6.2 生成测试报告

单纯的控制台输出不利于长期追踪。我们可以生成XML格式的报告(如JUnit格式),方便被Jenkins、GitLab CI等工具解析和展示。

  1. 使用GTest的XML输出 :运行测试时添加 --gtest_output=xml:report.xml 参数。
  2. 自定义报告 :在测试用例中,通过 WebDriverClient executeScript 方法,可以在失败时截取屏幕快照(通过WebDriver的 /screenshot 端点)和页面源代码,保存到文件,并附在测试报告里。这对于调试金融系统复杂的UI问题至关重要。

6.3 CI/CD流水线配置示例(GitLab CI)

# .gitlab-ci.yml
stages:
  - build
  - test

variables:
  CHROME_VERSION: "120.0.6099.130"

before_script:
  - apt-get update
  - apt-get install -y wget unzip
  # 安装指定版本的Chrome
  - wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
  - echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
  - apt-get update && apt-get install -y google-chrome-stable=$CHROME_VERSION
  # 安装匹配的ChromeDriver
  - wget https://chromedriver.storage.googleapis.com/`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_VERSION`/chromedriver_linux64.zip
  - unzip chromedriver_linux64.zip -d /usr/local/bin/
  - chmod +x /usr/local/bin/chromedriver

build_test:
  stage: build
  script:
    - mkdir build && cd build
    - cmake ..
    - make -j4
  artifacts:
    paths:
      - build/fin_auto_test

run_ui_tests:
  stage: test
  script:
    - cd build
    - ./fin_auto_test --gtest_output=xml:ui_test_report.xml
  artifacts:
    when: always
    paths:
      - build/ui_test_report.xml
      - build/*.png   # 收集可能产生的失败截图
    reports:
      junit: build/ui_test_report.xml

7. 常见问题排查与性能优化

在实际使用中,你会遇到各种问题。以下是一些典型问题及其解决方案。

7.1 常见问题速查表

问题现象 可能原因 排查步骤与解决方案
无法启动ChromeDriver/会话 1. ChromeDriver未在PATH中。
2. Chrome与ChromeDriver版本不匹配。
3. 端口被占用(默认9515)。
1. which chromedriver 检查。
2. 核对两者主版本号。
3. 换用其他端口,或在代码中先尝试终止已有进程。
元素找不到(NoSuchElement) 1. 页面未加载完成。
2. 元素在iframe内。
3. 选择器写错或元素属性动态生成。
1. 使用 waitForAndFindElement
2. 使用 driver.switchTo().frame() 切换到对应iframe(需在WebDriverClient中实现此方法)。
3. 使用浏览器开发者工具复查元素属性,尝试更稳定的选择器。
元素不可交互(ElementNotInteractable) 1. 元素被遮挡(如弹窗)。
2. 元素未处于可视区域。
3. 元素 disabled 属性为true。
1. 关闭遮挡物或等待其消失。
2. 滚动元素到视口(用 executeScript 执行JS滚动)。
3. 检查业务逻辑,等待元素变为可用状态。
脚本执行超时 1. 页面JS执行过久或死循环。
2. 网络请求慢。
3. WebDriver超时设置太短。
1. 优化测试脚本,避免执行复杂JS。
2. 增加 set_read_timeout
3. 在Capabilities中设置 pageLoadStrategy none eager ,不等待全页加载。
在CI无头模式下失败,本地却成功 1. 无头模式下的视口大小不同,导致页面布局变化。
2. CI环境缺少字体或某些依赖。
3. 资源加载策略不同。
1. 在Capabilities中明确设置窗口大小 --window-size=1920,1080
2. 在CI Docker镜像中安装必要的字体包(如 fonts-liberation )。
3. 增加等待时间,或使用 executeAsyncScript 处理异步加载。

7.2 性能优化建议

  1. 会话复用 :对于一组相关的测试用例(如测试同一个业务流程),不要每个用例都 startSession quit 。可以在测试夹具的 SetUp TearDown 中只做一次,但要注意测试之间的状态清理(如登出、清理Cookies)。
  2. 并行测试 :如果测试套件很大,可以考虑并行运行。需要为每个并行线程启动独立的ChromeDriver进程和浏览器会话,并管理好不同的端口号,避免冲突。可以使用线程池来管理。
  3. 禁用不必要的功能 :在启动Chrome时通过 goog:chromeOptions args 禁用图片加载( --blink-settings=imagesEnabled=false )、JavaScript ( --disable-javascript 慎用)、扩展程序等,可以显著加快页面加载速度,但需确保不影响测试功能。
  4. 选择器优化 :使用 id 选择器最快。尽量避免使用 * 通配符和复杂的CSS选择器。对于频繁查找的元素,可以在页面对象中缓存其 element_id (但要注意元素可能刷新导致ID失效)。
  5. 网络模拟与Mock :对于依赖外部数据源(如行情接口)的金融前端,可以在测试环境中使用网络拦截(通过DevTools Protocol,需额外实现)或直接Mock后端API,使测试更快速、稳定且不依赖外部环境。

7.3 稳定性提升:重试机制

网络抖动或前端框架的轻微渲染延迟可能导致偶发性失败。实现一个简单的重试机制能提升稳定性。

template<typename Func, typename... Args>
auto retry(int max_attempts, int delay_ms, Func&& func, Args&&... args) -> decltype(func(args...)) {
    for (int attempt = 1; attempt <= max_attempts; ++attempt) {
        try {
            return func(std::forward<Args>(args)...);
        } catch (const std::exception& e) {
            if (attempt == max_attempts) throw;
            std::cerr << "Attempt " << attempt << " failed: " << e.what() << ". Retrying..." << std::endl;
            std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
        }
    }
    throw std::runtime_error("All retry attempts failed.");
}

// 使用示例:在点击登录按钮时加入重试
retry(3, 1000, [&login_page]() {
    login_page.clickLogin();
});

这套用C++实现的Selenium自动化测试框架,虽然初期搭建比Python版本繁琐,但一旦成型,其与C++核心业务代码的无缝集成、卓越的运行性能以及在复杂CI环境下的可控性,为金融系统这类对稳定性和性能有严苛要求的领域提供了坚实可靠的自动化测试保障。它不仅仅是测试UI,更是连接前端展示与后端核心逻辑的自动化验证桥梁。

更多推荐