虽然我们的主题是 cdp(chrome debug protocol)的应用,但在介绍 cdp 之前,不得不先从 selenium 说起,因为这两者有密不可分的关系。
我们知道,在最新的 selenium 里,当你去执行一个测试动作,例如打开浏览器,然后输入网址,找到一个搜索框填入文本并点击搜索,这背后所依赖的技术,其实是 webdriver,而当你的动作执行在 chrome 浏览器上,更为细化的说,依赖的是 chromewebdriver。
我们详细的来分析这一流程,你会更清楚的知道 cdp 与此有何关系。
首先我们来写一个示例代码:
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.baidu.com/")
执行这段代码,会看到系统启动了 chrome 浏览器,并跳转到了百度首页。
看 driver = webdriver.Chrome() 这一句,以下是这段代码的流程图解。
可以看到在这个流程中,chromedriver 起到的是桥梁的作用,他接受客户端的请求,然后转化为浏览器的标准指令操作浏览器,而在后半部分,也就是指令如何让浏览器工作中,就涉及到了我们的主题 cdp,因为这部分的标准其实就是 cdp。
chrome debug protocol,简称 cdp。
大家应该都用过 chrome 浏览器的 F12,也就是 devtools,其实这是一个 web 应用,当你使用 devtools 的时候,浏览器本身会作为一个服务端,而你看到的浏览器调试工具界面,其实只是一个前端应用,在这中间通信的,就是 cdp,他是基于 websocket 的,一个让 devtools 和浏览器内核交换数据的通道。
cdp 本身是可开放的,换句话说,你用 devtools 能做什么(例如操作浏览器,获取网络信息,获取 js 覆盖数据,获取性能数据等等),你就能用 cdp 做什么。
cdp 的官方文档地址,可以点击查阅,这里再简单的介绍一下。
cdp 把不同的操作划分为了不同的域(domain),每个域负责不同的功能模块,例如,Page 域可以获取当前页面数据,或者操作页面跳转等等;Profiler 域可以获取当前的页面的 js 覆盖率数据等等;
直接引用FEX 的一篇文章来解释:
该协议把操作划分为不同的域 (domain),比如 DOM、Debugger、Network、Console 和 Timeline 等,可以理解为 DevTools 中的不同功能模块。
每个域 (domain) 定义了它所支持的 command 和它所产生的 event。
每个 command 包含 request 和 response 两部分,request 部分指定所要进行的操作以及操作说要的参数,response 部分表明操作状态,成功或失败。
command 和 event 中可能涉及到非基本数据类型,在 domain 中被归为 Type,比如:’frameId’: ,其中 FrameId 为非基本数据类型
至此,不难理解:
domain = command + event + type
最原始的使用 cdp 的方式可以参照 google 的 cdp 文档来:
1.使用附加参数打开 chrome 的远程调试协议开关(普通模式下的 chrome 浏览器是无法直接使用 cdp 通信的,另外,请注意,在不同的操作系统下指令细节会有所不同),
chrome.exe --remote-debugging-port = 9222
此时一个打开的远程调试协议的浏览器实例被启动。
2.为做演示,在打开的浏览器中,输入百度的网址并进入,新开一个 tab,进入网址http://localhost:9222,此时应该如截图所示:
3.点击百度这个标签,进入他的 devtools 界面,看一下地址栏,记录 page/后面的通信标识值,然后在 console 里输入以下代码:
var ws = new WebSocket('ws://localhost:9222/devtools/page/这里填刚才记录的标识值');
ws.send('{"id": 1, "method": "Page.navigate", "params": {"url": "http://www.soso.com"}}')
执行完会发现,刚才的百度页面,跳转到了 soso 的页面,其实这段代码就是新开了一个 websocket 连接到刚才的百度页面的调试地址,然后通过 page 域的 navigate 方法让该页面重新跳转到了指定地址。
需要注意的一点是,在这里,每个 tab(页面)都只有一个单独的通信地址,且每个地址只能与对应的 tab 通信。
以上就是比较原始的使用方法,实际上,cdp 有很多封装好的库可以使用,例如 python 的 PyChromeDevTools 库,nodejs 的 chrome-remote-interface 库等等,更多上层封装库请参见官方文档。
看了以上内容,可能你会得出一个结论,selenium 依赖 webdriver,而在 chrome 浏览器中,webdriver 又依赖 chromedriver,chromedriver 又是依赖 cdp 的;那么,我使用 selenium 和我直接使用 cdp,有什么区别呢?
实际上真要较真(不怕麻烦)的话,是没有区别的,但二者还是有一些差异的,selenium 的封装更为上层,使得你不用去关心原始的 cdp 到底如何使用,而且也集成了聚焦测试所需要的一些功能,例如分布式执行,docker image 等等,使得在测试这个需求上,更为方便;而直接使用 cdp 的话,会让整个结构更为简洁,而且,有些操作由于 webdriver 没有封装(例如获取性能数据,获取 js 覆盖率等等),所以直接使用 cdp 会更为精准。那么有没有办法让二者的优点结合呢?
在这里我发现了两种方案可以做到,
1.通过命令行启动开启了调试协议的 chrome 浏览器,然后在 selenium 里,初始化 webdriver 时指定 ChromeOption 的__debugger_address 的值为之前的远程调试地址,然后使用 selenium 操作 webdriver,使用 PyChromeDevTools 操作 cdp,示例代码如下:
import os
import PyChromeDevTools
from selenium import webdriver
cmd = "chrome.exe --remote-debugging-port=9222"
os.popen(cmd) #此时chrome浏览器打开
time.sleep(3)
chrome = PyChromeDevTools.ChromeInterface()#使用chrome操作cdp
options = webdriver.ChromeOptions()
options._debugger_address = "localhost:9222"
driver = webdriver.Chrome(chrome_options=self.options)
2.可以直接使用 selenium 的预留 cdp 通信方法 execute_cdp_cmd,示例代码如下:
from selenium import webdriver
driver = webdriver.Chrome()
driver.get("https://www.baidu.com/")
driver.execute_cdp_cmd('Page.navigate',{"url": "http://www.soso.com"})
有时候当脚本出错了,我们会希望获得更多的信息去排查,如果这时候能重现当时的网络请求,那么排查会容易的多,下面是一个获取页面网络数据(response 值)的例子,这里只拿了请求的 response 值,但实际上稍加改动就可以把请求信息拿全(request+response),为了方便演示上面两种方法,这里混用了上面的两个方案。
from selenium import webdriver
import time
import os
import PyChromeDevTools
os.chdir(r"C:\Users\zyj\AppData\Local\Google\Chrome SxS\Application") #这里是改变了当前环境变量
cmd = "chrome.exe --remote-debugging-port=9222"
os.popen(cmd)#启动chrome浏览器
time.sleep(3)
chrome = PyChromeDevTools.ChromeInterface()
options = webdriver.ChromeOptions()
options._debugger_address = "localhost:9222"
driver = webdriver.Chrome(chrome_options=options)
chrome.Network.enable()#开启页面的网络信息收集模式
time.sleep(2)
driver.execute_cdp_cmd('Page.navigate',{"url": "http://www.mycaigou.com"})#跳转到示例站点,这里用的selenium的execute_cdp_cmd方法做到的
responseReceived = chrome.wait_event("Network.responseReceived", timeout=60)#等待response收集事件结束,获取收集信息,这里的信息不包含详细的response内容,需要用到方法getResponseBody
resquest_id = responseReceived[0]['params']['requestId']#这个id是指你想要收集哪个请求的信息,他是请求的唯一标示,这里随便拿了一个,没做遍历
res = chrome.Network.getResponseBody(requestId=resquest_id)#传入id,拿到请求的返回值
print(res)
这是PyChromeDevTools的官方例子,演示如何获取页面加载时间:
import PyChromeDevTools
import time
import os
os.chdir(r"C:\Users\zyj\AppData\Local\Google\Chrome SxS\Application") #这里是改变了当前环境变量
cmd = "chrome.exe --remote-debugging-port=9222"
os.popen(cmd)#启动chrome浏览器
chrome = PyChromeDevTools.ChromeInterface()
chrome.Network.enable()
chrome.Page.enable()
start_time=time.time()
chrome.Page.navigate(url="http://www.baidu.com/")
chrome.wait_event("Page.loadEventFired", timeout=60)#loadEventFired是页面全部加载完毕的时间,实际上这里还可以用reload方法,选择去除缓存加载,这样的时间会更加精确
end_time=time.time()
print ("Page Loading Time:", end_time-start_time)
在 cdp 中,是无法直接得到覆盖率的数据的,有关 js 代码执行情况的统计,在 Profiler 域,我们可以使用 takePreciseCoverage 方法来拿到 js 执行数据,这个数据的数据结构是这样的:
'result': {
'result': [{
'scriptId': '17',
'url':'https://www.xxxxxxxxx.com/browser/guide.js',
'functions': [{
'functionName': 'get',
'ranges': [{
'startOffset': 0,
'endOffset': 4273,
'count': 1
}],
'isBlockCoverage': False
},
}],
}],
}
......
一个 result 包含多个 js 的统计情况,每个 url 基本就是 js 的请求地址;在每个 js 的统计情况里,又有多个 function 的统计情况,每个 function 里的 startOffset 和 endOffset 指的是这个方法的被统计语句按字节位置来算的开始位置和结束位置,count 代表这段语句是否被执行到,1 代表是,0 代表否。
因此,思路就是,拿到测试完成后的 js 统计数据,然后通过每个 js 统计数据里的每个 function 的统计坐标值和统计状态,和原始 js 数据比对,从而实现对 js 覆盖状况的总览。
这个实现比较复杂,我直接做成了一个模块,只需要接受 takePreciseCoverage 的数据,就可以计算出覆盖情况并直观的展示,具体的代码在github上,这里就不放出了。
最终的效果图演示:
相关的技术文档介绍参见:利用 cdp 拿到自动化测试后的 js 覆盖率数据并展示