ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++ Web自动化测试实战:HTTP接口、WebDriver协议与嵌入式设备验证

C++ Web自动化测试实战:HTTP接口、WebDriver协议与嵌入式设备验证 说实话我第一次被领导要求“用C写Web自动化测试”的时候内心是拒绝的。圈子里聊Web自动化测试默认就是PythonpytestSelenium或者Java那套接口自动化框架C这个选项几乎没人提。直到后来接手一个纯C的边缘网关项目测试同学为了回归Web接口硬生生维护了两套数据模型线上接口改个字段两边都要同步改一个漏改就是事故。那之后我才下定决心直接在C工程里搭一套自动化测试。几年下来这套用C做的HTTP接口验证、Web页面驱动、甚至嵌入式Web设备巡检的方案已经在我们好几个业务线上稳定跑着了。今天这篇博文不打算绕弯子直接把常用函数、场景化应用指南、以及我踩过的坑完整拆出来。内容会覆盖HTTP接口测试、WebDriver协议底层调用、ESP32这类嵌入式内嵌Web网页的自动化验证还有CI集成的工程化细节。不管你是刚入门的C学习者想找练手方向还是已经被测试方案困住的工程开发者应该都能从中找到能直接抄作业的部分。1. 为什么用C做Web自动化测试不只是“反主流”1.1 什么场景下你才需要它首先要说明C做Web自动化测试不是要替代pytest或者Selenium而是它的适用场景非常特殊。我整理了一下真正适合让C出场的通常跑不掉下面三种情况。第一种被测对象本身就是C写的服务。比如我们做的是边缘网关固件核心协议、业务逻辑全在C层外部暴露的是HTTP/REST接口。这种情况下测试同学用Python就得重新维护一套接口封装、一套数据模型服务端C结构体一改Python那侧马上断层。与其这样不如让测试代码直接复用产品工程里的库同一个CMake构建系统同一套编译产物字段类型天然对齐接口变更在编译期就能暴露一大部分问题。第二种性能敏感的高并发回归。Python写脚本虽然快但一到高并发场景就容易受GIL限制进程线程模型也没那么轻。C配合线程池几十个线程同时打HTTP请求内存和CPU开销都比同等Python方案低一个量级。我们曾经做过一个接口压测回归同样的200并发C客户端能跑到接近打满服务端Python脚本反而先因为GIL变成了瓶颈。第三种嵌入式Web设备比如ESP32内嵌的Web管理页面。很多IoT设备出厂后板子上根本没有Python运行时但C交叉编译出来的静态测试工具往固件里一塞就能跑。即使不在设备上跑在宿主机上通过WiFi直接访问设备IP做自动化C写出来的测试程序也比Python脚本更少依赖部署到CI执行节点几乎没有环境障碍。当然我也得泼一盆冷水。如果是纯粹的前端UI探索性测试或者产品原型阶段需要快速堆一堆临时脚本那就别拿C硬刚。C的优势是稳定、可控、贴近被测系统代价是编译调试周期长、动态性差。适合它的场景是那些已经有稳定C技术栈、或者对性能和运行时环境有硬约束的团队。1.2 C Web自动化测试的技术栈全貌决定用C之后你需要先建立一张自己的技术地图。我按测试类型整理了一张选型表下面的实操内容基本都围绕这张表展开。测试类型常用组件说明HTTP/REST接口测试cpp-httplib、libcurlcpp-httplib头文件库API简单适合快速落地libcurl底层控制力最强需要自己处理回调JSON解析与断言nlohmann/json现代C最舒服的JSON库没有之一AT运算、迭代、序列化都够用Web UI自动化直接调WebDriver协议利用ChromeDriver提供的REST接口C只写HTTP客户端即可控制浏览器WebSocket测试Boost.Beast websocket做实时功能验收需要处理握手、帧收发和二进制流测试框架与断言GoogleTest、Catch2GTest宏丰富、JUnit报告成熟Catch2更轻单头文件就能跑构建与依赖管理CMake、vcpkg/FetchContent让C测试工程和产品工程共用一套构建体系依赖不再靠手拖头文件这张表里HTTP客户端、JSON处理、测试框架是日常最高频的三件套先把它们练熟基本就能覆盖绝大多数接口自动化测试需求。WebDriver协议、WebSocket这些属于遇到具体场景再深入研究的部分后面第3章会分开讲。2. 常用函数全解析接口测试三板斧接口自动化测试说白了就三件事发HTTP请求、解析返回值、做断言断言比较。把这三件事对应的常用函数吃透你的测试代码地基就稳了。这一章我按使用频率从高到低逐个说明。2.1 HTTP请求cpp-httplib 的Get/Post/Deletecpp-httplib是一个头文件为主的轻量HTTP库接口设计非常现代对从没写过C网络代码的人也很友好。我用它做接口测试的频率最高原因很简单不需要理解底层Socket、不需要写回调一个Client对象直接调方法就能拿到完整响应。常用的核心方法就这几个cli.Get(path, headers)发GET请求cli.Post(path, body, content_type)发POST请求body传JSON字符串content_type传application/json即可cli.Put(path, body, content_type)、cli.Delete(path)对应HTTP的PUT和DELETEres-statusHTTP状态码res-body响应体文本res.error()请求层面的错误枚举配合httplib::to_string()能输出可读信息一个典型的GET接口测试长这样#include httplib.h #include nlohmann/json.hpp #include gtest/gtest.h using json nlohmann::json; TEST(ApiTest, GetUserInfo) { httplib::Client cli(https://api.example.com); cli.set_connection_timeout(5, 0); // 5秒连接超时 cli.set_read_timeout(10, 0); // 10秒读超时 auto res cli.Get(/users/42, {{Accept, application/json}}); ASSERT_TRUE(res) 网络错误: httplib::to_string(res.error()); ASSERT_EQ(res-status, 200); json body json::parse(res-body); EXPECT_EQ(body.at(id).getint(), 42); EXPECT_TRUE(body.contains(name)); }这里有两个细节特别容易踩坑。第一个是超时设置set_connection_timeout管的是建立TCP连接的时间set_read_timeout管的是拿到响应体前允许的最长等待。测试环境里服务偶发慢超时设太短会出现“半路扇”的假失败设太长又会拖慢整条用例集我一般连接5秒、读取10秒起步针对慢接口再单独放宽。第二个是断言前一定要用ASSERT_TRUE(res)判断请求本身是否成功。很多人先写ASSERT_EQ(res-status, 200)一旦网络失败程序直接解引用空指针崩溃连错误信息都来不及打调试效率极低。这是新手最常犯的错。2.2 底层兜底libcurl 的核心函数与回调cpp-httplib虽然好用但有些场景它不够灵活。比如要精确控制证书校验、要跟踪重定向次数、要往请求里塞自定义Header但不想构造httplib::Headers那么直白或者要复用一套成熟的连接池逻辑。这时候我通常会绕到libcurl。libcurl是C语言库函数的套路比较古老核心函数必须记清楚curl_global_init(CURL_GLOBAL_ALL)全局初始化整个程序生命周期里调用一次即可curl_easy_init()创建句柄curl_easy_setopt(curl, CURLOPT_xxx, ...)设置URL、超时、回调等curl_easy_perform(curl)同步执行请求curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, status_code)拿HTTP状态码curl_easy_cleanup(curl)释放句柄最需要注意的是响应体怎么收集。libcurl默认把数据丢给stdout如果你想拿到字符串必须提供一个写回调。我一般这样写size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) { output-append(static_castchar*(contents), size * nmemb); return size * nmemb; } std::string http_get(const std::string url) { CURL* curl curl_easy_init(); std::string response; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, response); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); curl_easy_perform(curl); curl_easy_cleanup(curl); return response; }原生libcurl用起来确实啰嗦所以我建议不管在哪个项目里都把它包一层RAII类构造时初始化、析构时清理别裸奔CURL*。裸指针一旦中间有异常抛出内存泄漏几乎是必然的。2.3 JSON处理nlohmann/json 的高频操作与陷阱接口测试里JSON就是主战场。nlohmann/json这个库用起来非常顺手但有几个高频函数和对应的坑必须提前讲清楚。常用操作有这些json::parse(str)把字符串解析成JSON对象解析失败会抛json::parse_error异常body.at(key)取指定key的值key不存在会抛出json::out_of_range异常body.contains(key)判断key是否存在body.dump()把JSON对象序列化成字符串body.is_object()、body.is_array()类型判断方法用来做结构校验使用上最容易翻车的是运营商方括号body[key]和at()的选择。很多人图省事用body[key]但它的行为是如果key不存在mud中会主动插入一个null值。这在测试代码里非常危险等于把一个“请求返回JSON里缺字段”的缺陷掩盖成了“字段值为null”的误判。我统一要求项目里只准用at()和contains()组合宁可抛异常也要让缺字段的问题第一时间爆出来。再补一个我常用的结构校验写法if (body.contains(data) body.at(data).is_object()) { const auto data body.at(data); EXPECT_TRUE(data.contains(list)); } else { FAIL() 响应缺少data对象 body.dump(); }2.4 断言与测试框架GoogleTest常用宏最后一块拼图是断言。GTest的宏语法简单基本看一遍就能会。我日常用最多的是这些EXPECT_EQ(actual, expected)、ASSERT_EQ判等ASSERT_系列失败会立刻终止当前用例EXPECT_TRUE(cond)、EXPECT_FALSE(cond)布尔判断EXPECT_NE(a, b)判不等EXPECT_THROW(statement, exception_type)验证某段代码抛指定异常这个在测异常路径时特别好用FAIL() 自定义信息直接失败并输出信息TEST(ApiTest, GetUserInfo)这个宏定义了一个测试用例。如果用例之间需要共享初始化逻辑就用TEST_F加fixture把公共的Client初始化和Token获取放到SetUp()里避免每个用例重复一遍。很多人在断言里只写EXPECT_EQ(body.at(code).getint(), 200)失败后只看到“期望200实际500”完全不知道是哪段逻辑的问题。我建议所有关键断言后面都加一段流输出比如 接口响应体: body.dump(2)失败时能直接把完整响应打出来排查效率完全是两个量级。3. 场景化实战从接口到浏览器再到嵌入式设备学会了基础函数还得看它们怎么组合起来解决实际问题。我挑了四个最典型的场景展开每个场景都来自真实项目的复盘。3.1 RESTful接口回归登录、Token与数据驱动绝大多数Web系统的接口自动化第一步都是登录拿凭证。以常见的Token认证为例测试流程是这样先用一个测试账号POST登录接口拿到JSON里的Token后续所有业务接口在Header里带上Token再校验响应。代码骨架长这样class ApiFixture : public ::testing::Test { protected: void SetUp() override { cli std::make_uniquehttplib::Client(https://api.example.com); cli-set_connection_timeout(5, 0); cli-set_read_timeout(10, 0); auto res cli-Post(/api/login, json{{username, tester01}, {password, Test123}}.dump(), application/json); ASSERT_TRUE(res); ASSERT_EQ(res-status, 200); token json::parse(res-body).at(data).at(token).getstd::string(); } std::unique_ptrhttplib::Client cli; std::string token; }; TEST_F(ApiFixture, GetDeviceList) { auto res cli-Get(/api/devices, {{Authorization, Bearer token}}); ASSERT_TRUE(res); ASSERT_EQ(res-status, 200); json body json::parse(res-body); ASSERT_TRUE(body.at(data).is_array()); EXPECT_GT(body.at(data).size(), 0); }这里有个工程化要点登录请求的地址和账号不能写死在代码里。我可以从环境变量读取也可以在CMake里通过编译宏注入。这样测试部署到不同环境测试环境、预发环境时不用重新编译就能切换目标服务。再进一步就是数据驱动。把用例的输入、期望结果抽到JSON或表格文件里测试代码循环读取并逐条执行。比如测试订单创建的参数校验就把“缺少金额”“金额为负数”“商品ID不存在”这些case都放在一张表里每条数据对应一个子测试。C没有Python参数化装饰器那么方便但我用std::vectorTestCase配合EXPECT_*做循环断言效果一样而且失败时能通过SCOPED_TRACE定位到具体是哪条数据出的错。3.2 Web UI测试用C直调WebDriver协议控制浏览器很多人可能不知道Selenium之所以能控制浏览器底层靠的是一套WebDriver协议。这个协议本质上就是HTTP REST接口ChromeDriver在本地端口默认9515监听测试进程发HTTP请求给它它再反过来驱动Chrome执行动作。So既然它是HTTP接口C直接用我们在第2章学的HTTP客户端就能操作浏览器。这个过程可以概括成四个请求第一步创建Session。POST/sessionbody里带浏览器能力参数响应里会返回sessionId。httplib::Client driver(http://127.0.0.1, 9515); auto res driver.Post(/session, R({capabilities:{alwaysMatch:{browserName:chrome}}}), application/json); ASSERT_TRUE(res res-status 200); std::string sessionId json::parse(res-body) .at(value).at(sessionId).getstd::string();第二步导航到目标页面。POST/session/{sessionId}/urlbody是{url:https://example.com}。第三步执行操作或获取信息。比如获取页面标题GET/session/{sessionId}/title解析响应里value字段就行。要点击元素的话得先POST定位元素再POST点击路径类似/session/{sessionId}/element和/session/{sessionId}/element/{elementId}/click。第四步收尾。DELETE/session/{sessionId}把浏览器会话关掉避免Chrome进程残留。用这套玩法我们纯C项目里也能做最基本的Web UI冒烟测试。比如验证登录页面能正常加载、标题正确、关键元素存在这些步骤全部通过HTTP请求闭环不需要在测试机额外装Python环境。要注意的是ChromeDriver和Chrome的版本必须匹配否则创建Session会直接报错。这个版本匹配很容易被忽略CI镜像里升级了Chrome忘了升级ChromeDriver整套UI用例瞬间全红。建议不管是本地还是CI都用固定的Chrome版本并在启动脚本里加一层版本检查。3.3 嵌入式Web设备自动化ESP32内嵌网页的验证方式很多IoT设备比如ESP32做的智能网关内部会嵌一个Web管理页面用户在浏览器里配置WiFi、查看状态、升级固件。这类设备做自动化测试最大的麻烦是运行环境五花八门很多板子根本不支持Python。C在这里的优势体现得淋漓尽致。我常用的做法是在宿主机上用C写一套测试程序通过WiFi或局域网直连设备IP对着它的HTTP接口发请求。举个例子验证设备配置下发TestDeviceConfig() { httplib::Client cli(http://192.168.1.100); cli.set_connection_timeout(3, 0); cli.set_read_timeout(5, 0); auto res cli.Post(/api/config, json{{ssid, MyHome}, {password, passw0rd}}.dump(), application/json); ASSERT_TRUE(res); auto retry_res cli.Get(/api/config); ASSERT_TRUE(retry_res); json cfg json::parse(retry_res-body); EXPECT_EQ(cfg.at(ssid).getstd::string(), MyHome); }这种嵌入式设备测试有两个和普通Web服务很不一样的地方。第一个是响应延迟波动大设备芯片性能弱处理一个JSON请求可能要好几百毫秒并发能力也弱。所以测试程序对单个设备的请求并发度要控制在很低水平超时要放宽不能套用云端服务的3秒超时标准。第二个是设备网络不稳定WiFi偶发性丢包请求失败不能直接判定产品Bug要设计重试机制把“设备没响应”和“设备响应错误”区分开。我在用例里会保留请求次数和重试日志最终报告中能清楚看到哪些失败是网络抖动导致的。如果要把设备接进CI最稳的做法是硬件在环HIL一台专门的测试工位放着一台真实设备CI流水线在宿主机上编译C测试程序跑完把结果传到服务器。虽然不如纯软件测试轻量但对嵌入式Web设备来说这已经是最接近真实用户体验的自动化方案了。3.4 WebSocket实时功能测试C也能直播测WebSocket在实时视频、消息推送、协作编辑这些Web场景里用得越来越多。常规的HTTP接口测试覆盖不到实时连接那部分于是我也用C补上了这块。Boost.Beast里有WebSocket客户端实现代码上手需要适应一下Asio的模型但基本套路很清晰先解析URL、建立TCP连接、再做WebSocket握手之后就可以互相收发文本帧了。伪代码流程大概是这样// 1. 解析 ws://host:port/path // 2. tcp::iostream 连接 host:port // 3. beast::websocket::stream 包装连接 // 4. ws.handshake(host, path) // 5. ws.write(文本帧或二进制帧) // 6. beast::flat_buffer buffer; ws.read(buffer) // 7. 从 buffer 取出数据断言字段我踩过最深的坑是握手的Host字段必须和服务端要求的完全一致尤其是端口。服务端是Spring Boot集成WebSocket的场景对Origin和Host校验很严格写错一个就返回403握手失败。排查的时候直接看服务端日志比在客户端侧瞎猜效率高。另外WebSocket测试里超时策略很重要。实时通道不像HTTP那样请求-响应一一对应服务端可能主动推消息过来客户端也可能长时间收不到数据。测试脚本必须能等待异步消息我一般用Asio的steady_timer做轮询式超时在5秒内没等到预期消息就直接判失败同时打印缓冲区里最近收到的几条消息方便定位是服务端没推还是推了但内容不对。4. 工程化落地CI、环境搭建与问题排查函数和场景都有了最后一步是让这套东西在团队里真正跑起来。没有CI、没有依赖管理、没有排错手段测试代码再漂亮也活不过三个版本。4.1 从零搭建测试工程VSCode CMake vcpkg我建议测试工程不要手动拖头文件那会让依赖版本完全失控。用CMake做构建依赖交给vcpkg或者FetchContent两条路都行。vcpkg适合正式项目版本锁定清晰FetchContent适合快速起步直接在CMake脚本里拉Git Tag。这是一份可以直接参考的CMakeLists.txtcmake_minimum_required(VERSION 3.20) project(web_auto_test CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(CURL REQUIRED) find_package(OpenSSL REQUIRED) find_package(GTest REQUIRED) include(FetchContent) FetchContent_Declare(httplib URL https://github.com/yhirose/cpp-httplib/archive/refs/tags/v0.15.3.tar.gz) FetchContent_MakeAvailable(httplib) FetchContent_Declare(json URL https://github.com/nlohmann/json/archive/refs/tags/v3.11.3.tar.gz) FetchContent_MakeAvailable(json) add_executable(test_runner test_main.cpp api_test.cpp) target_compile_definitions(test_runner PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) target_link_libraries(test_runner PRIVATE httplib::httplib nlohmann_json::nlohmann_json GTest::gtest_main CURL::libcurl OpenSSL::SSL)如果你正在用VSCode开发配置C/C环境这步不复杂装好C/C扩展CMake插件会自动读取上面的CMakeLists并生成构建任务。记得在.vscode/settings.json里把cmake.configureArgs加上-DCMAKE_TOOLCHAIN_FILEvcpkg根目录/scripts/buildsystems/vcpkg.cmake如果走vcpkg不然依赖会找不到。Windows上如果要把测试产物拷到别的机器跑注意目标机器要装匹配的VC运行库否则双击就是经典的0xc000007b报错。4.2 深坑排查常见问题与速查表我整理了一份自己在实战中反复遇到的排查速查表几乎覆盖了C Web自动化测试的绝大部分“第一次就跪”的问题。现象原因解决办法编译找不到httplib.hinclude路径没配好或FetchContent未拉取成功检查CMake是否有target_link_libraries(httplib::httplib)VSCode重新加载CMake缓存链接报错找不到curl未find_package(CURL)或未链接库加find_package(CURL REQUIRED)并链接CURL::libcurlHTTPS请求报SSL证书错误测试环境常用自签名证书证书链不信任测试环境可临时enable_server_certificate_verification(false)正式环境应配置CA路径json::parse抛异常服务端返回非JSON文本比如返回了HTML错误页解析前打印res-body前几百字符用ASSERT_NO_THROW包裹解析WebDriver连接报session not createdChromeDriver版本与Chrome版本不匹配锁定版本号检查localhost:9515是否被占用接口返回中文乱码响应头Content-Type缺少charsetutf-8服务端补全响应头客户端不要盲目做编码转换按UTF-8字节处理请求偶发Timeout测试环境网络不稳或设备处理慢适当调大set_read_timeout增加重试机制重试间隔加随机抖动这里面最值得单独说一句的是SSL证书问题。很多团队第一次跑HTTPS接口测试就被自签名证书卡住然后一刀切把证书验证关掉一直带到生产环境。我的经验是测试环境关掉可以但要在代码里留一个显眼的开关并且带上注释“仅限测试环境”防止谁脑子一热把这段搬到生产压测脚本里。4.3 C方案和pytest/Selenium怎么选最后聊聊选型。我不否认Python的pytest和Selenium是Web自动化测试的事实标准它们生态成熟、脚本短、上手快该用就用。但C这套方案也有明确的一席之地结合我的经验两者并不冲突。对比维度Ccpp-httplib GTestPythonpytest Selenium上手成本需要C和CMake基础编译期较长脚本即写即跑入门门槛低运行性能高高并发场景能打满服务端受GIL限制高并发需要多进程绕路与C被测系统集成天然无缝可复用产品库需要跨语言绑定或维护双套模型UI探索测试可以做但动态调试弱生态成熟元素定位、截图报告开箱即用CI集成编译产物单一部署简单需要保证Python环境依赖一致典型场景接口回归、性能压测、嵌入式设备验证前端UI变化多的探索性测试、快速原型验证我的判断标准很简单被测系统本身是C技术栈或者对性能和环境依赖有硬约束那就用C如果是普通Web应用的UI回归尤其是页面结构经常调整的前端项目那就让pytest和Selenium上。工具是为人服务的不是用来证明谁的方案更酷的。有一点对面试也特别有用当你把WebDriver协议底层是REST这个点讲清楚再补充一段用C直调ChromeDriver的代码思路面试官通常会眼前一亮因为这证明你不只是会用某个框架而是理解了自动化测试的本质。这套东西在自动化测试面试题里绝对是个加分项。写在最后的实操体会真要说起来“该不该用C做Web自动化测试”这个问题本身没有标准答案。我自己在实际项目里最后还是把C用在接口回归、性能脚本和嵌入式设备验证上真正偏页面UI的探索性测试比如频繁变动的前端交互逻辑还是交给了Python那套方案。这不是能力不够而是每个工具都有自己的边界。如果你准备在自己的项目里试水我给一个最小可行的起步路径先用cpp-httplib加GoogleTest搭一个能跑通GET接口的测试工程跑进CI然后把登录态的获取逻辑加进fixture再逐步覆盖你负责模块的核心接口。整个起步过程不用追求大而全但一定要把断言写规范、把失败信息打清楚。测试代码写多了你就会发现真正难的往往不是用什么语言而是你有没有把一个场景拆到位、把每个断言背后的业务逻辑吃透。
返回列表