前言先说一个必须纠正的前提Selenium 官方没有提供 C 语言绑定。标题里「C 方向 Selenium 实战」这个组合如果理解成「引入一个 C 版的 Selenium 库然后跟着写」是不成立的——Selenium 官方维护的绑定只有 Java、Python、C#、Ruby、JavaScript 和 Kotlin 这几种C 不在其中。但这件事有解而且解法比想象中干净。Selenium 的核心并不是某个语言的库而是W3C WebDriver 协议一套基于 HTTP JSON 的远程控制协议。浏览器厂商提供实现了该协议的驱动driver进程比如 ChromeDriver、geckodriver、msedgedriver。你要做的事情本质上就是发 HTTP 请求、解析 JSON 响应。任何能发 HTTP 的语言都能做C 当然也行。所以本文的路线是先讲清自动化测试的分层和 WebDriver 协议的本质再用 C 加上一个 HTTP 客户端把请求发出去最后讲工程结构怎么组织才不至于三个月后没人敢改。本文以 C17 为基准。语言官方绑定C 可用的方案Java / Python / C# / Ruby / JS / Kotlin有不适用C没有直接用 WebDriver 协议 HTTP 客户端或使用第三方封装库任意语言不适用直接实现 W3C WebDriver 协议一、自动化测试的分层与 Selenium 的定位自动化测试按覆盖范围大致分三层层次测什么速度稳定性典型工具单元测试单个函数/类极快极高GoogleTest、Catch2集成测试模块间协作、接口中高各类 API 测试框架端到端测试E2E真实浏览器里的完整流程慢低Selenium、Playwright、CypressSelenium 属于最上面那层。它的价值是「从用户视角验证真实浏览器里的行为」——这是单元测试永远覆盖不到的前端路由跳转、跨域、Cookie 与 SameSite 策略、真实渲染时序。它的代价也很明确慢、脆。一个按钮的 id 改了几十个用例一起红。因此第一条工程原则E2E 用例要少而精。把绝大多数逻辑正确性交给单元测试和集成测试E2E 只覆盖最关键的几条主流程登录、下单、支付这类。反过来把什么都往 E2E 里塞是自动化测试项目失败最常见的原因。在 C 项目里这个分工尤其自然核心逻辑本来就有 GoogleTest 覆盖E2E 只用来验证「Web 端能不能跑通」。二、W3C WebDriver 协议Selenium 的本质理解这一点C 做 Web 自动化就不再别扭了。WebDriver 协议的基本形态启动一个驱动进程比如 ChromeDriver它监听一个本地端口比如 9515。你的测试代码向http://localhost:9515发 HTTP 请求。驱动进程去操作真实浏览器把结果以 JSON 返回。关键端点具体字段以 W3C WebDriver 规范文本为准动作方法路径新建会话POST/session关闭会话DELETE/session/{session id}打开网址POST/session/{session id}/url取当前网址GET/session/{session id}/url取页面标题GET/session/{session id}/title查找元素POST/session/{session id}/element点击元素POST/session/{session id}/element/{element id}/click输入文本POST/session/{session id}/element/{element id}/value请求体和响应体都是 JSON。查找元素时请求体形如{using: css selector, value: #login-btn}响应里返回的元素标识是一个固定键名这是规范定的{value: {element-6066-11e4-a52e-4f735466cecf: 0f1e2d3c-...}}看到这里就明白了所谓「Selenium 的 C 支持」本质上就是「C 的 HTTP 客户端 JSON 解析」。没有任何魔法。三、用 C 发起 WebDriver 请求C 标准库没有 HTTP 客户端和 JSON 解析器C17 的std::filesystem管不了网络网络库 TS 也没进标准。所以需要第三方库。最常见的组合是libcurl做 HTTP nlohmann/json做 JSON两者都是成熟且广泛使用的库。下面这段代码演示用 libcurl 新建一个 WebDriver 会话。编译需要链接 libcurl// 依赖 libcurl链接时加 -lcurlMSVC 下为 libcurl.lib #include curl/curl.h #include iostream #include string static std::size_t write_cb(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) { auto* out static_caststd::string*(userdata); out-append(ptr, size * nmemb); return size * nmemb; } int main() { if (curl_global_init(CURL_GLOBAL_DEFAULT) ! CURLE_OK) { std::cerr curl_global_init 失败\n; return 1; } CURL* curl curl_easy_init(); if (!curl) { std::cerr curl_easy_init 失败\n; curl_global_cleanup(); return 1; } // 请求体让驱动启动 Chrome const char* body R({capabilities:{alwaysMatch:{browserName:chrome}}}); struct curl_slist* headers nullptr; headers curl_slist_append(headers, Content-Type: application/json); std::string response; curl_easy_setopt(curl, CURLOPT_URL, http://localhost:9515/session); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 30000L); CURLcode rc curl_easy_perform(curl); if (rc ! CURLE_OK) { std::cerr 请求失败: curl_easy_strerror(rc) \n; } else { long status 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status); std::cout HTTP 状态码: status \n; std::cout 响应: response \n; } curl_slist_free_all(headers); curl_easy_cleanup(curl); curl_global_cleanup(); return 0; }几个容易被忽略的点curl_global_init必须在任何其他 curl 调用之前执行且只执行一次。多线程下还要注意线程安全边界具体以 libcurl 官方文档为准。curl_slist是链表curl_slist_append返回的可能是新的头指针首个调用时所以必须用返回值接住不能写curl_slist_append(headers, ...)然后丢掉返回值。会话 id 需要从响应 JSON 里取出来后续所有请求的路径都带着它。至于 JSON 解析nlohmann/json的用法是nlohmann::json::parse(字符串)它会把结果解析成一个可下标访问的对象。这是第三方库的接口不是标准库的一部分用之前请以该库文档为准。如果你连第三方依赖都不想引用最朴素的字符串查找也能取到sessionId字段的值——脆弱但零依赖。四、工程结构把测试写成可维护的样子一个 E2E 测试如果写成「一长串发请求」三个月后没人敢动。基本的结构化手段是Page Object页面对象模式把「页面上的元素和操作」封装成类测试用例只调用业务语义的方法不关心选择器。下面这段是只依赖标准库的完整可编译示例用一个假的驱动实现演示这个结构。真实项目里把FakeDriver换成基于 libcurl 的实现即可测试代码本身一行都不用改。这也说明了 Page Object 的价值它把「协议细节」和「业务断言」隔离开了。#include iostream #include map #include string #include vector // ---- 抽象驱动接口真实实现走 W3C WebDriver 协议 ---- class Driver { public: virtual ~Driver() default; virtual void get(const std::string url) 0; virtual std::string title() 0; virtual bool click(const std::string selector) 0; virtual bool type(const std::string selector, const std::string text) 0; virtual std::string text(const std::string selector) 0; }; // ---- 假驱动仅用于演示结构不是真实实现 ---- class FakeDriver : public Driver { public: void get(const std::string url) override { at_login_ url.find(/login) ! std::string::npos; elements_[#login-btn] {true, }; elements_[#msg] {true, }; } std::string title() override { return at_login_ ? 登录页 : 首页; } bool click(const std::string selector) override { auto it elements_.find(selector); if (it elements_.end() || !it-second.first) return false; if (selector #login-btn) elements_[#msg].second 登录成功; return true; } bool type(const std::string selector, const std::string text) override { auto it elements_.find(selector); if (it elements_.end() || !it-second.first) return false; it-second.second text; return true; } std::string text(const std::string selector) override { auto it elements_.find(selector); return it elements_.end() ? std::string{} : it-second.second; } private: struct Element { bool present; std::string value; }; bool at_login_ false; std::mapstd::string, Element elements_; }; // ---- Page Object把选择器关在类里 ---- class LoginPage { public: explicit LoginPage(Driver d, std::string base) : d_(d), base_(std::move(base)) {} void open() { d_.get(base_ /login); } std::string title() { return d_.title(); } bool submit() { return d_.click(#login-btn); } std::string message() { return d_.text(#msg); } private: Driver d_; std::string base_; }; // ---- 测试用例只表达业务语义 ---- int main() { FakeDriver driver; LoginPage page(driver, http://localhost:8080); page.open(); if (page.title() ! 登录页) { std::cerr FAIL: 标题不符\n; return 1; } if (!page.submit()) { std::cerr FAIL: 提交按钮不可点击\n; return 1; } if (page.message() ! 登录成功) { std::cerr FAIL: 提示信息不符实际 page.message() \n; return 1; } std::cout PASS\n; return 0; }这段代码可以直接编译运行输出PASS。它的意义在于展示了依赖倒置测试代码依赖抽象的Driver真实驱动和假驱动都实现它。这样一来无头模式下跑真浏览器、本地用假驱动快速验证逻辑切换成本几乎为零。五、CI 集成与稳定性把 E2E 放进 CI持续集成要处理三件事无头模式。CI 机器通常没有显示器Chrome 要用 headless 相关参数启动。具体参数名以对应浏览器版本的文档为准它们会变。显式等待不要sleep。用固定sleep(3)等待元素出现是 E2E 脆弱性的头号来源慢的机器上不够快的机器上白等。WebDriver 协议本身提供了等待机制各种「隐式等待」设置和「预期条件」模式应当优先使用。失败留证据。用例失败时截图、保存页面源码、打印当前 URL比一句「元素找不到」有用得多。常见坑点坑点 1以为 Selenium 有官方 C 绑定。❌ 在构建脚本里找一个 selenium-cpp 官方包 —— 找不到官方不提供✅ 直接用 W3C WebDriver 协议HTTP 请求 JSON 解析用 libcurl 这类通用 HTTP 库坑点 2驱动进程没起来就发请求。// ❌ 直接连 9515 端口ChromeDriver 没启动时连接被拒 // curl_easy_perform 返回 CURLE_COULDNT_CONNECT但很多人只看响应体不看返回值// ✅ 先检查 curl_easy_perform 的返回值和 HTTP 状态码 CURLcode rc curl_easy_perform(curl); if (rc ! CURLE_OK) { std::cerr 连接层失败: curl_easy_strerror(rc) \n; }坑点 3忘了释放curl_slist。// ❌ 每次请求都 append 到同一个 slist 却不释放长跑必然内存增长 // for (...) { headers curl_slist_append(headers, Content-Type: application/json); }// ✅ 用完释放多次请求可复用同一个 slist但同一份头别重复添加 curl_slist_free_all(headers);坑点 4会话没关闭进程越积越多。// ❌ 测试结束直接 return浏览器和驱动进程留在后台// ✅ 用 RAII 把「关闭会话」绑到对象生命周期上 class Session { public: explicit Session(std::string id) : id_(std::move(id)) {} ~Session() { /* 这里发 DELETE /session/{id_} */ } Session(const Session) delete; Session operator(const Session) delete; private: std::string id_; };坑点 5用固定 sleep 等待元素。// ❌ 脆弱机器慢就误报机器快就白等 // std::this_thread::sleep_for(std::chrono::seconds(3)); // auto el driver.find_element(#result);// ✅ 轮询等待直到超时真实实现应使用 WebDriver 的等待机制 // 伪代码最多等 N 秒每 100ms 重试一次查找找到即返回超时则报错坑点 6选择器写得太依赖实现细节。❌ div div:nth-child(3) span a 前端一改布局用例全红而且看不出这是哪个业务元素✅ 给关键元素加稳定的>// ❌ Page Object 里塞 std::cerr FAIL职责混乱无法复用// ✅ Page Object 只提供操作与取值断言留在测试用例里 bool ok page.submit(); // 测试侧if (!ok) { ... }坑点 8以为抓到元素就能立刻点击。元素「存在」不等于「可交互」可能被遮挡、可能在动画中、可能还没绑定事件。直接点击会抛「元素不可交互」一类错误。正确做法是等待「可点击」状态而不是等待「存在」。总结要点结论Selenium 与 C官方没有C 绑定但可以用 W3C WebDriver 协议协议本质HTTP JSON任何能发 HTTP 的语言都能用推荐技术栈libcurl 做 HTTP 第三方 JSON 库如 nlohmann/json分层原则E2E 用例少而精逻辑正确性交给单元/集成测试组织方式Page Object 模式隔离选择器与业务断言稳定性显式等待替代固定 sleep失败留截图与页面源码资源管理用 RAII 保证会话关闭、curl_slist释放C 做 Web 自动化测试并不别扭别扭的只是「想找一个现成的 C Selenium 库」这个期待。把心理模型从「调库」换成「发协议请求」剩下的就是常规的工程问题抽象好驱动接口、封装好页面对象、管理好资源生命周期、把等待写对。这几件事做扎实C 的 E2E 测试一样可以稳定跑在 CI 上。