Selenium自动化测试集成mitmproxy流量监控:从UI操作到接口验证的完整实践
1. 项目概述为什么我们需要在自动化测试中监控流量做自动化测试尤其是Web UI自动化Selenium是绕不开的工具。它能模拟用户点击、输入、滚动帮你完成一整套业务流程的验证。但不知道你有没有遇到过这种场景测试脚本跑得飞起页面元素也都找到了结果业务逻辑就是不对。比如点击“提交订单”按钮页面没报错但订单就是没生成。这时候你第一反应是什么肯定是去看后端接口到底有没有被调用、传了什么参数、返回了什么数据对吧传统的做法是手动打开浏览器的开发者工具F12切换到Network标签页然后重新跑一遍测试眼睛死死盯着那一行行飞速滚动的请求记录。这效率太低了而且对于持续集成CI环境中的无头浏览器测试你根本没法实时看到这些网络流量。另一种更“硬核”的做法是在测试代码里到处埋点用日志记录请求和响应。但这不仅侵入性强代码变得臃肿而且对于HTTPS请求、WebSocket连接等复杂场景几乎无从下手。所以这个项目的核心价值就出来了将网络流量监控无缝集成到Selenium自动化测试流程中。我们不再依赖手动抓包也不需要在业务代码里“打补丁”而是通过一个中间人代理mitmproxy让所有从Selenium控制的浏览器发出的网络请求都“流经”我们的监控程序。这样我们就能以编程的方式实时捕获、分析、甚至修改每一个HTTP/HTTPS请求和响应。这对于测试数据验证、接口异常模拟、性能监控、甚至是安全测试都是一个降维打击。简单来说它解决了自动化测试中的一个关键痛点业务验证的深度不足。UI自动化只能断言“页面元素是否存在”而结合流量监控我们可以断言“是否发出了正确的API调用”、“接口返回的数据结构是否符合预期”。这相当于给你的自动化测试装上了一双“透视眼”。2. 核心工具选型与架构设计要实现这个目标我们需要两个核心工具Selenium和mitmproxy。它们的角色分工非常明确。2.1 Selenium前端的“执行者”Selenium WebDriver 是我们的自动化执行引擎。它通过浏览器驱动如ChromeDriver与真实浏览器交互执行我们编写的脚本。在这个项目中Selenium除了完成常规的UI操作外还有一个关键任务将其控制的浏览器的网络代理指向我们搭建的mitmproxy服务。这样浏览器的所有流量才会被引导到我们的监控程序。为什么是Selenium因为它生态成熟、支持语言多Python, Java, C#等、浏览器兼容性好。虽然也有Puppeteer、Playwright等后起之秀但Selenium目前仍然是企业级自动化测试中最普遍的选择社区资源和解决方案也最丰富。2.2 mitmproxy后端的“观察者”与“干预者”mitmproxy 是一个基于Python的、支持SSL/TLS的交互式HTTP代理。它名字里的“mitm”就是“中间人”Man-in-the-Middle的缩写。它的强大之处在于透明解密HTTPS流量通过向浏览器安装由mitmproxy生成的自签名CA证书它可以解密并查看HTTPS请求的内容而不会触发浏览器的安全警告在测试环境下。可编程的拦截与修改你可以编写Python脚本addons定义规则来拦截特定的请求或响应并实时修改其内容如状态码、头部、Body。多种交互模式除了命令行交互模式它还提供Web界面mitmweb和直接嵌入Python代码的能力非常适合集成到自动化框架中。在这个架构里mitmproxy扮演了流量枢纽的角色。Selenium驱动浏览器发出的请求先到达mitmproxymitmproxy可以记录、分析或修改后再转发给真实的服务器服务器的响应也先回到mitmproxy再返回给浏览器。我们编写的监控逻辑就运行在mitmproxy这个环节。整体数据流如下Selenium测试脚本-启动并配置浏览器代理-浏览器-所有网络请求-mitmproxy代理服务器-可选执行我们的监控/修改脚本-真实目标服务器。3. 环境搭建与核心配置详解理论讲清楚了我们开始动手。这里以Python语言和Chrome浏览器为例这是目前最主流的组合。3.1 基础环境准备首先安装必要的Python包。建议使用虚拟环境venv进行隔离。pip install selenium mitmproxymitmproxy这个包会同时安装mitmproxy,mitmdump,mitmweb三个命令行工具。我们主要用mitmdump来以脚本模式运行代理。3.2 生成并信任mitmproxy的CA证书这是能监控HTTPS流量的前提。如果不安装证书浏览器会对mitmproxy解密的HTTPS连接报安全错误。启动一次mitmproxy在命令行运行mitmdump然后按CtrlC停止它。这会在用户目录下生成默认的配置和证书文件通常在~/.mitmproxyLinux/macOS或C:\Users\用户名\.mitmproxyWindows。找到证书文件在上述目录中找到mitmproxy-ca-cert.cer或mitmproxy-ca-cert.pem文件。这个就是根证书。将证书导入系统或浏览器系统级导入将证书导入到操作系统的受信任根证书颁发机构。这样所有浏览器都会信任。具体步骤因操作系统而异Windows的证书管理器macOS的钥匙串访问。浏览器级导入更推荐在测试时使用。以Chrome为例启动时通过Selenium指定用户数据目录--user-data-dir然后手动打开该浏览器访问http://mitm.it选择对应的操作系统图标下载证书并安装到浏览器。注意安装时一定要选择“信任此证书以标识网站”。重要提示这个自签名证书仅用于测试环境。绝对不要在生产环境或访问真实敏感信息的浏览器中安装此证书。测试完成后请从受信任的证书列表中移除它。3.3 编写mitmproxy监控脚本Addon这是整个项目的核心逻辑所在。我们将创建一个Python脚本定义如何“消费”流经代理的流量。创建一个文件比如叫flow_monitor.py#!/usr/bin/env python3 Selenium自动化测试流量监控插件 from mitmproxy import http, ctx class SeleniumFlowMonitor: def __init__(self): self.request_list [] # 用于存储请求信息方便测试脚本查询 self.target_pattern /api/ # 只监控包含/api/的请求可按需修改 def request(self, flow: http.HTTPFlow): 请求到达代理时触发 url flow.request.pretty_url method flow.request.method # 示例只记录我们关心的API请求 if self.target_pattern in url: request_info { url: url, method: method, headers: dict(flow.request.headers), query: dict(flow.request.query), timestamp: flow.request.timestamp_start, } # 如果有请求体如POST的JSON也记录下来 if flow.request.content: try: # 尝试解码为JSON如果不是则存为文本 import json request_info[body] json.loads(flow.request.content.decode(utf-8)) except: request_info[body] flow.request.content.decode(utf-8, errorsignore) self.request_list.append(request_info) ctx.log.info(f捕获请求: {method} {url}) # 示例干预请求 - 为特定请求添加一个自定义Header if /api/user/login in url: flow.request.headers[X-Test-Injected] Selenium-Monitor # 示例干预请求 - 修改请求体例如固定测试账号 # if /api/order/create in url and flow.request.content: # import json # try: # body json.loads(flow.request.content) # body[userId] test_user_001 # 强制使用测试用户ID # flow.request.text json.dumps(body) # except: # pass def response(self, flow: http.HTTPFlow): 响应从服务器返回即将发给浏览器时触发 url flow.request.pretty_url if self.target_pattern in url: status_code flow.response.status_code ctx.log.info(f收到响应: {status_code} for {url}) # 示例基于响应内容进行断言这里只是记录实际断言在测试脚本中 if status_code ! 200: ctx.log.warn(f请求 {url} 返回异常状态码: {status_code}) # 示例干预响应 - 模拟服务器错误用于测试前端容错 # if /api/payment/callback in url: # flow.response.status_code 500 # flow.response.text {error: Internal Server Error, code: 500} def get_captured_requests(self, filter_urlNone): 供外部测试脚本调用的方法获取捕获的请求列表 if filter_url: return [req for req in self.request_list if filter_url in req[url]] return self.request_list def clear_captured_requests(self): 清空捕获的请求列表通常在每个测试用例开始前调用 self.request_list.clear() # 创建addons实例 addons [ SeleniumFlowMonitor() ]这个脚本定义了一个SeleniumFlowMonitor类它有两个核心方法request(): 每个请求经过时调用我们可以记录、修改请求。response(): 每个响应返回时调用我们可以记录、修改响应。我们还提供了get_captured_requests和clear_captured_requests方法这样我们的Selenium测试脚本就能和这个监控插件“通信”获取它收集到的数据。3.4 配置Selenium使用mitmproxy代理现在我们需要在Selenium启动浏览器时告诉它“请把你的所有网络请求都发送到mitmproxy那里去。”from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver_with_proxy(proxy_host127.0.0.1, proxy_port8080): 创建一个配置了mitmproxy代理的Chrome WebDriver实例 chrome_options Options() # 关键配置设置代理服务器地址和端口mitmproxy默认监听8080 proxy_server f{proxy_host}:{proxy_port} chrome_options.add_argument(f--proxy-serverhttp://{proxy_server}) # 为了避免一些SSL警告和确保代理正常工作通常需要添加以下参数 chrome_options.add_argument(--ignore-certificate-errors) chrome_options.add_argument(--allow-running-insecure-content) # 使用固定的用户数据目录方便之前安装的证书生效 # chrome_options.add_argument(r--user-data-dirC:\path\to\your\chrome\test\profile) # 如果是无头模式也建议加上 # chrome_options.add_argument(--headless) # 禁用自动化控制标志降低被网站检测的风险可选 chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionschrome_options) # 进一步隐藏WebDriver特征可选 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); }) return driver这样创建的driver其产生的所有HTTP/HTTPS流量除了可能配置了直连的地址都会经过运行在127.0.0.1:8080的mitmproxy。4. 整合实战一个完整的测试用例示例让我们把上面所有的部分串起来写一个完整的测试场景测试一个模拟登录流程并验证登录API是否被正确调用。第1步启动mitmproxy并加载我们的插件不是在命令行手动启动我们在测试脚本中用子进程启动它这样生命周期可以和测试用例绑定。import subprocess import time import requests from selenium import webdriver # 启动mitmproxy进程 mitm_process subprocess.Popen( [mitmdump, -s, flow_monitor.py, --set, block_globalfalse, -q], stdoutsubprocess.PIPE, stderrsubprocess.PIPE ) # 等待代理服务器启动 time.sleep(3) print(mitmproxy代理服务器已启动。)参数解释-s flow_monitor.py: 指定加载我们写的插件脚本。--set block_globalfalse: 允许代理连接互联网默认是只拦截不转发。-q: 安静模式减少控制台输出。第2步编写测试脚本并与监控插件交互这里需要一个技巧我们的测试脚本主进程如何与mitmproxy插件子进程里的SeleniumFlowMonitor实例通信一个简单有效的方法是使用HTTP API。我们需要稍微修改一下flow_monitor.py让它启动一个简单的HTTP服务器来提供数据查询接口。我们在flow_monitor.py的SeleniumFlowMonitor类里增加一个方法并用一个独立线程启动Flask服务需要安装Flaskpip install flask# 在 flow_monitor.py 文件末尾追加 from flask import Flask, jsonify import threading monitor_instance addons[0] # 获取我们创建的插件实例 app Flask(__name__) app.route(/captured_requests, methods[GET]) def get_requests(): API端点获取所有捕获的请求 filter_param request.args.get(filter) requests monitor_instance.get_captured_requests(filter_param) return jsonify(requests) app.route(/clear_requests, methods[POST]) def clear_requests(): API端点清空捕获的请求 monitor_instance.clear_captured_requests() return jsonify({status: cleared}) def run_api_server(): app.run(host127.0.0.1, port5001, debugFalse, use_reloaderFalse) # 在插件加载完成后启动API服务器线程 def start_api_server(): api_thread threading.Thread(targetrun_api_server, daemonTrue) api_thread.start() ctx.log.info(监控数据API服务器已在 http://127.0.0.1:5001 启动。) # 我们需要一个事件来知道mitmproxy何时启动完毕这里用一个简单的加载完成标记 # 更优雅的方式是使用mitmproxy的running事件这里为了简化我们在插件初始化后延迟启动 import threading import time def delayed_start(): time.sleep(2) start_api_server() threading.Thread(targetdelayed_start, daemonTrue).start()第3步完整的测试用例脚本test_login.pyimport subprocess import time import requests from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import pytest class TestLoginWithTrafficMonitor: classmethod def setup_class(cls): 整个测试类开始前执行启动mitmproxy cls.mitm_process subprocess.Popen( [mitmdump, -s, flow_monitor.py, --set, block_globalfalse, -q], stdoutsubprocess.PIPE, stderrsubprocess.PIPE ) time.sleep(5) # 等待mitmproxy及其内部的API服务器启动 print( 测试环境准备就绪mitmproxy代理已启动。) classmethod def teardown_class(cls): 整个测试类结束后执行关闭mitmproxy if cls.mitm_process: cls.mitm_process.terminate() cls.mitm_process.wait() print( mitmproxy代理已关闭。) def setup_method(self): 每个测试方法开始前执行启动浏览器并清空之前的监控数据 # 1. 清空监控插件中记录的旧请求数据 try: requests.post(http://127.0.0.1:5001/clear_requests) except: print(警告无法连接到监控API可能未启动或端口冲突。) # 2. 创建使用代理的浏览器驱动 self.driver self._create_driver_with_proxy() self.wait WebDriverWait(self.driver, 10) def teardown_method(self): 每个测试方法结束后执行关闭浏览器 if self.driver: self.driver.quit() def _create_driver_with_proxy(self): chrome_options webdriver.ChromeOptions() chrome_options.add_argument(--proxy-serverhttp://127.0.0.1:8080) chrome_options.add_argument(--ignore-certificate-errors) # 其他优化选项... driver webdriver.Chrome(optionschrome_options) return driver def test_login_api_called_correctly(self): 测试用例验证用户登录操作是否正确触发了登录API并且API参数正确。 # 步骤1访问登录页面 self.driver.get(https://your-test-app.com/login) # 步骤2输入用户名和密码 username_input self.wait.until( EC.presence_of_element_located((By.ID, username)) ) password_input self.driver.find_element(By.ID, password) username_input.send_keys(test_user) password_input.send_keys(test_password_123) # 步骤3点击登录按钮 login_button self.driver.find_element(By.XPATH, //button[contains(text(),登录)]) login_button.click() # 步骤4等待登录完成例如跳转到首页或出现成功提示 self.wait.until( EC.url_contains(/dashboard) # 假设登录成功跳转到仪表盘 ) print( UI层面登录成功页面已跳转。) # --- 关键步骤从流量监控中获取证据 --- # 等待一下确保网络请求已经发生并被捕获 time.sleep(2) # 调用监控API获取捕获到的所有请求 try: response requests.get(http://127.0.0.1:5001/captured_requests) captured_requests response.json() except Exception as e: pytest.fail(f无法从监控API获取数据: {e}) # 步骤5断言 - 是否存在登录API的请求 login_api_requests [req for req in captured_requests if /api/user/login in req[url]] assert len(login_api_requests) 0, 未捕获到登录API请求 print(f 流量监控成功捕获到 {len(login_api_requests)} 个登录API请求。) # 取最新的一个登录请求进行详细断言 latest_login_req login_api_requests[-1] # 断言1请求方法为POST assert latest_login_req[method] POST, f登录请求方法错误应为POST实际为{latest_login_req[method]} # 断言2请求体中包含正确的用户名和密码 request_body latest_login_req.get(body, {}) assert isinstance(request_body, dict), 登录请求体不是JSON格式 assert request_body.get(username) test_user, f请求用户名不匹配: {request_body.get(username)} # 注意密码可能被加密这里断言取决于实际业务逻辑 # assert request_body.get(password) is not None, 请求中缺少密码字段 # 断言3请求头中可能包含我们插件注入的测试Header参见flow_monitor.py中的示例 headers latest_login_req.get(headers, {}) assert X-Test-Injected in headers, 未在请求头中找到测试注入的Header assert headers[X-Test-Injected] Selenium-Monitor print( 所有流量断言通过登录API调用符合预期。) # 步骤6可选进一步断言响应数据。这需要我们在插件中也记录响应并通过API暴露。 # 这里仅作示意假设我们扩展了API能获取特定请求的响应 # login_response get_response_for_request(latest_login_req[id]) # assert login_response[status_code] 200 # assert sessionId in login_response[body]这个测试用例清晰地展示了如何将UI操作Selenium与后端接口验证mitmproxy流量监控无缝结合。测试不仅通过了页面跳转还从网络层面验证了“登录”这个业务动作确实触发了正确的后端交互。5. 高级技巧与避坑指南在实际项目中应用这套方案你会遇到一些挑战。下面是我踩过坑后总结的经验。5.1 处理证书信任问题尤其是无头模式和CI环境在无头模式Headless或Docker/CI环境中浏览器没有图形界面手动安装证书变得不可能。解决方案将mitmproxy CA证书预置到浏览器信任库对于Chrome可以启动时通过--ignore-certificate-errors参数忽略证书错误但这会放过所有错误不安全。更好的方式是将mitmproxy-ca-cert.pem文件转换为.crt格式并在启动浏览器时通过--ssl-client-certificate-file和--ignore-certificate-errors-spki-list指定证书公钥来信任它。这需要一些额外的步骤处理SPKI。使用系统级信任推荐用于可控的测试环境在构建测试镜像Dockerfile时就将mitmproxy的根证书添加到系统的受信任根证书存储中。这样容器内所有应用都会自动信任。Ubuntu/Debian示例COPY mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt RUN update-ca-certificates然后Chrome启动时就不需要额外参数了。使用mitmproxy的--ssl-insecure参数启动mitmproxy时加上这个参数它会自动生成临时证书而不验证CA但浏览器仍会报警需配合--ignore-certificate-errors使用仅用于快速调试。5.2 过滤噪音流量聚焦测试目标一个现代前端页面会加载大量资源JS、CSS、图片、字体、第三方统计等。如果不加过滤监控插件会记录海量无关请求不仅浪费内存也增加分析难度。优化策略在插件中过滤就像我们示例代码里的if self.target_pattern in url:只处理包含特定路径如/api/,/graphql的请求。使用mitmproxy的过滤表达式启动mitmdump时可以使用-f参数指定过滤表达式例如mitmdump -s script.py -f /api/.*只拦截URL匹配/api/.*的流量。这能减少进入插件的数据量。区分静态资源和API通常静态资源有固定的路径后缀.js,.css,.png,.woff2可以在插件中排除它们。5.3 并发测试与数据隔离当多个测试用例并行运行时它们共享同一个mitmproxy实例和监控插件会导致捕获的请求数据混在一起无法区分是谁产生的。解决方案为每个测试进程启动独立的mitmproxy实例这是最干净的方式。每个测试用例或测试进程绑定一个独立的代理端口如8081, 8082...并启动自己的mitmproxy进程和插件实例。管理起来稍复杂但隔离性最好。在请求/响应中注入测试标识在Selenium测试开始时通过插件API设置一个当前测试的“会话ID”。在插件的request()方法中为每个捕获的请求都打上这个ID标签。查询时通过会话ID来筛选。这要求插件能处理多组数据。在测试用例级别清空数据就像我们示例中的clear_captured_requests在每个setup_method中清空列表。这只适用于串行执行的测试。5.4 性能影响与稳定性mitmproxy作为中间代理会对网络请求引入额外的延迟。对于性能敏感或超时严格的测试这可能成为问题。优化建议使用--stream参数启动mitmproxy时使用mitmdump --stream 100k对于大于100KB的响应体mitmproxy不会将其全部加载到内存中而是流式传输可以降低内存占用和延迟。精简插件逻辑在request()和response()方法中避免进行复杂的同步操作如网络IO、大量计算。如果需要进行耗时的断言或分析可以考虑将数据放入队列由后台线程异步处理。避免监控所有流量严格使用过滤规则只关注测试相关的接口。确保代理服务器资源充足在CI服务器上确保运行mitmproxy的机器有足够的CPU和内存。5.5 处理WebSocket和gRPC流量默认情况下mitmproxy对HTTP/HTTPS的拦截能力很强但对于WebSocket和gRPC这类长连接、二进制协议支持度会有所不同。WebSocketmitmproxy可以拦截WebSocket的连接建立HTTP Upgrade请求并查看其握手信息。对于后续的WebSocket数据帧mitmproxy默认是透传的但可以通过编写专门的插件来拦截和修改WebSocket消息操作flow.websocket对象。gRPCgRPC基于HTTP/2mitmproxy可以拦截其流量。但由于gRPC使用Protocol Buffers二进制编码直接查看是乱码。你需要了解.proto文件定义并在插件中使用相应的库如grpcio-tools来解码和编码消息才能进行有意义的监控和断言。6. 典型应用场景扩展掌握了基础能力后这套组合拳可以在测试的多个维度发挥巨大作用。6.1 接口契约测试Contract TestingUI测试脚本执行时自动验证前端与后端交互是否符合接口文档Swagger/OpenAPI的定义。做法在监控插件中集成一个OpenAPI Schema验证器如prance或openapi-spec-validator。当捕获到API请求和响应时实时根据预定义的Schema验证其URL、方法、参数类型、请求/响应体结构是否符合约定。一旦发现不符立即记录为测试失败。6.2 模拟后端异常与混沌测试在不修改后端代码的情况下测试前端应用对异常情况的处理是否健壮。模拟慢响应在插件的request()方法中对特定接口加入time.sleep(5)模拟网络延迟或后端处理缓慢。模拟错误状态码在response()方法中将特定接口的flow.response.status_code改为 500、502、404等并修改响应体为错误信息验证前端是否展示友好的错误提示。模拟数据异常修改响应体中的关键数据例如将订单金额改为负数、将日期格式改为非法字符串检查前端是否会有JS错误或显示异常。6.3 性能数据采集自动化收集关键业务操作的网络性能指标。做法在插件中记录每个请求的flow.request.timestamp_start和flow.response.timestamp_end计算耗时。可以统计一个测试场景如“下单流程”内所有API的耗时分布、总耗时并输出报告。这比在浏览器中用Performance API采集更稳定且能关联到具体的业务接口。6.4 安全测试辅助检查前端应用是否存在不安全的网络实践。检测敏感信息泄露在response()方法中检查响应体是否包含明文密码、身份证号、密钥等通过正则表达式匹配。检查不安全的通信记录所有请求的URL检查是否有非HTTPS的请求在测试环境下可能允许但应被记录。验证CSRF Token检查关键POST请求是否携带了正确的CSRF Token头部或参数。这套Selenium mitmproxy的方案将UI自动化测试从“界面黑盒”升级为了“网络透视”极大地增强了测试的深度和可靠性。它需要一些初始的设置成本但一旦搭建完成就能为你的质量保障体系提供一个强大的、可编程的观察和干预层。