ITPub博客

基于Selenium和ChromeDriver的自动化页面性能测试

原创 Linux操作系统 作者:zhuyiquan90 时间:2018-06-28 17:48:07 0 删除 编辑

由于最近工作一直很紧张,拖了很久才在五一假期将Selenium实现自动化页面性能测试的代码实现部分补上,希望今后自己能更勤勉,多一些知识产出。 
Selenium WebDriver(以下简称SW)提供了一套用于Web应用程序的自动化测试工具。SW按其应用场景不同可以分为(1)基于HtmlUnit的无界面实现,并非驱动真实浏览器进行测试;(2)模拟真实输入,对多浏览器的支持和测试,包括FirefoxDriver、InternetExplorerDriver、OperaDriver和ChromeDriver;(3)对移动应用的测试,包括AndroidDriver和iPhoneDriver。 
针对SW进行功能性测试的文章和书已经很多了,比如如何操作获取页面元素内容。而本文所要写的是如何基于Selenium和ChromeDriver做页面性能测试,比如获取页面请求的加载时间、获取页面的DOM元素加载完成时间等等。类似于一些成熟的拨测产品的实现原型(这也是笔者正在做的项目)。我想这是非常有意义的一次探索。



1. Maven依赖

首先,项目需要引入依赖的相关selenium包:selenium-api和selenium-java,要考虑不同版本和JDK版本的兼容性,笔者是JDK 1.8。

<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-api --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-api</artifactId> <version>3.5.3</version> </dependency>
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
<!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>3.5.3</version> </dependency>
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2、ChromeDriver使用详解

本节内容参考https://sites.google.com/a/chromium.org/chromedriver/home,另外ChromeDriver的安装,笔者在《CentOS 7.x环境下搭建: Headless chrome + Selenium + ChromeDriver 实现自动化测试》中有详述。

2.1、DesiredCapabilities & ChromeOptions

Capabilities属性可以定义和配置你的ChromeDriver会话,以满足对应功能和需求。 
在Java实现中,类ChromeOptions和类DesiredCapabilities都可以用于具体定义Capabilities。 
比如以下代码,通过ChromeOptions来定义Chrome的window-size属性:

// 设置chromedriver路径 System.setProperty("webdriver.chrome.driver","/opt/drivers/chromedriver");

ChromeOptions options = new ChromeOptions(); // 设置chrome启动时size大小 options.addArguments("--window-size=1980,1000"); // 根据ChromeOptions实例化ChromeDriver WebDriver driver = new ChromeDriver(options); try { // 打开苏宁易购 driver.get("https://www.suning.com");
    } catch (Exception e) {
    e.printStackTrace();
    } finally { // 关闭浏览器 driver.quit();
}
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

当然,以上例子也可以改写为通过DesiredCapabilities来实现:

// 设置chromedriver路径 System.setProperty("webdriver.chrome.driver","/opt/drivers/chromedriver");

ChromeOptions options = new ChromeOptions(); // 设置chrome启动时size大小 options.addArguments("--window-size=1980,1000");
DesiredCapabilities cap = DesiredCapabilities.chrome();
cap.setCapability(ChromeOptions.CAPABILITY, options); // 根据DesiredCapabilities实例化ChromeDriver WebDriver driver = new ChromeDriver(cap); try { // 打开苏宁易购 driver.get("https://www.suning.com");
    } catch (Exception e) {
    e.printStackTrace();
    } finally { // 关闭浏览器 driver.quit();
}
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

2.2、Performance Log

ChromeDriver支持性能日志(Performance Log)数据的采集。想想看Chrome的F12控制台,我们能够采集到”Network”、Page”等,而这些是实现页面性能测试的基础。 
Performance Log并非是默认开启的属性,所以我们可以通过上节说的DesiredCapabilities在创建新会话的时候开启Performance Log。 
而采集到的日志,我们可以通过LogEntry对象输出到Console。具体代码实现如下:

package com.suning.webdrivertest.chromedemo; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.logging.LogEntry; import org.openqa.selenium.logging.LogType; import org.openqa.selenium.logging.LoggingPreferences; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; import java.util.logging.Level; /**
 *
 * Created by zhuyiquan90 on 2018/1/3.
 */ public class ChromeDriverDemo1 { public static void main(String[] args) { // 设置chromedriver路径 System.setProperty("webdriver.chrome.driver", "/opt/drivers/chromedriver");

        DesiredCapabilities cap = DesiredCapabilities.chrome();
        LoggingPreferences logPrefs = new LoggingPreferences(); // 启用Performance Log日志采集 logPrefs.enable(LogType.PERFORMANCE, Level.ALL);
        cap.setCapability(CapabilityType.LOGGING_PREFS, logPrefs); // 根据DesiredCapabilities实例化ChromeDriver WebDriver driver = new ChromeDriver(cap); try { // 打开苏宁易购 driver.get("https://www.suning.com"); for (LogEntry entry : driver.manage().logs().get(LogType.PERFORMANCE)) { // 输出采集到的性能日志 System.out.println(Thread.currentThread().getName() + entry.toString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally { // 关闭浏览器 driver.quit();
        }
    }
} 
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

其输出结果如下:

Starting ChromeDriver 2.34.522932 (4140ab217e1ca1bec0c4b4d1b148f3361eb3a03e) on port 29777 Only local connections are allowed.
四月 30, 2018 3:06:27 下午 org.openqa.selenium.remote.ProtocolHandshake createSession
信息: Detected dialect: OSS
main[2018-04-30T15:06:27+0800] [INFO] {"message":{"method":"Page.frameAttached","params":{"frameId":"49C70573CE1145CEB5B38A270213A48","parentFrameId":"28DAFE9FE90E9292F1B8EDB3315608EC","stack":{"callFrames":[{"columnNumber":240,"functionName":"","lineNumber":0,"scriptId":"21","url":""}]}}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"}
main[2018-04-30T15:06:27+0800] [INFO] {"message":{"method":"Page.frameStartedLoading","params":{"frameId":"49C70573CE1145CEB5B38A270213A48"}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"}
main[2018-04-30T15:06:27+0800] [INFO] {"message":{"method":"Page.frameNavigated","params":{"frame":{"id":"49C70573CE1145CEB5B38A270213A48","loaderId":"EE699DC52C8ACA226069D24DC92E16","mimeType":"text/html","name":"chromedriver dummy frame","parentId":"28DAFE9FE90E9292F1B8EDB3315608EC","securityOrigin":"://","url":"about:blank"}}},"webview":"28DAFE9FE90E9292F1B8EDB3315608EC"} 
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.3、Chrome DevTools Protocol View

这一节,我们来讲讲Network和Page包含的内容,即针对上一节输出的内容,我们如何有效利用,通过它们来计算页面性能(参考Chrome DevTools Protocol)。

2.3.1、Network

Network中我们用到的事件主要是requestWillBeSent、responseReceived、loadingFailed和loadingFinished四种:

Network.requestWillBeSent

当页面即将发送HTTP请求时触发,其Json格式为:

{
    "message": {
        "method": "Network.requestWillBeSent",
        "params": {
            "documentURL": "about:blank",
            "frameId": "C80F96297F4216E35079CFD86251AB8B",
            "initiator": {
                "lineNumber": 0,
                "type": "parser",
                "url": "https://www.suning.com/" },
            "loaderId": "58DDB2CF16600EAE484A541DF9440089",
            "redirectResponse": {
                "connectionId": 639,
                "connectionReused": false,
                "encodedDataLength": 497,
                "fromDiskCache": false,
                "fromServiceWorker": false,
                "headers": {
                    "Cache-Control": "no-cache",
                    "Connection": "keep-alive",
                    "Content-Length": "0",
                    "Date": "Mon, 30 Apr 2018 07:06:42 GMT",
                    "Expires": "Thu, 01 Jan 1970 00:00:00 GMT",
                    "Location": "https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cm",
                    "P3P": "CP=\"NON DSP COR CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa CONa HISa TELa OTPa OUR UNRa IND UNI COM NAV INT DEM CNT PRE LOC\"",
                    "Pragma": "no-cache",
                    "Server": "nginx/1.10.2",
                    "Set-Cookie": "CMBMP=IWl; Domain=.ipinyou.com; Expires=Thu, 10-May-2018 07:06:42 GMT; Path=/" },
                "headersText": "HTTP/1.1 302 Found\r\nServer: nginx/1.10.2\r\nDate: Mon, 30 Apr 2018 07:06:42 GMT\r\nContent-Length: 0\r\nConnection: keep-alive\r\nCache-Control: no-cache\r\nPragma: no-cache\r\nExpires: Thu, 01 Jan 1970 00:00:00 GMT\r\nP3P: CP=\"NON DSP COR CURa ADMa DEVa TAIa PSAa PSDa IVAa IVDa CONa HISa TELa OTPa OUR UNRa IND UNI COM NAV INT DEM CNT PRE LOC\"\r\nSet-Cookie: CMBMP=IWl; Domain=.ipinyou.com; Expires=Thu, 10-May-2018 07:06:42 GMT; Path=/\r\nLocation: https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cm\r\n\r\n",
                "mimeType": "",
                "protocol": "http/1.1",
                "remoteIPAddress": "127.0.0.1",
                "remotePort": 1086,
                "requestHeaders": {
                    "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
                    "Accept-Encoding": "gzip, deflate, br",
                    "Accept-Language": "zh-CN,zh;q=0.9",
                    "Connection": "keep-alive",
                    "Cookie": "sessionId=I4UF6b1WcgGMC; PYID=I4UF6b1Wcg99; CMTMS=p7Ik3Ve; CMSTMS=p7Ik3Ve; CMPUB=ADV-DefaultAdv; CMBMP=IW2",
                    "Host": "cm.ipinyou.com",
                    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" },
                "requestHeadersText": "GET /baidu/cms.gif?baidu_error=1&timestamp=1525072001 HTTP/1.1\r\nHost: cm.ipinyou.com\r\nConnection: keep-alive\r\nUser-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36\r\nAccept: image/webp,image/apng,image/*,*/*;q=0.8\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: sessionId=I4UF6b1WcgGMC; PYID=I4UF6b1Wcg99; CMTMS=p7Ik3Ve; CMSTMS=p7Ik3Ve; CMPUB=ADV-DefaultAdv; CMBMP=IW2\r\n",
                "securityDetails": {
                    "certificateId": 0,
                    "cipher": "AES_256_GCM",
                    "issuer": "RapidSSL SHA256 CA",
                    "keyExchange": "ECDHE_RSA",
                    "keyExchangeGroup": "P-256",
                    "protocol": "TLS 1.2",
                    "sanList": ["*.ipinyou.com", "ipinyou.com"],
                    "signedCertificateTimestampList": [{
                        "hashAlgorithm": "SHA-256",
                        "logDescription": "Symantec log",
                        "logId": "DDEB1D2B7A0D4FA6208B81AD8168707E2E8E9D01D55C888D3D11C4CDB6ECBECC",
                        "origin": "Embedded in certificate",
                        "signatureAlgorithm": "ECDSA",
                        "signatureData": "3045022024364934CBC90A8529E327E6EF853E3EF5E48B7F1598414E0F10059DC92685FC022100A74F93A8CF23D6572D7597C072368D69EC43AFB6A9EDAA4B01B43921AADEFDC2",
                        "status": "Verified",
                        "timestamp": 1511173770857.0 }, {
                        "hashAlgorithm": "SHA-256",
                        "logDescription": "Google 'Pilot' log",
                        "logId": "A4B90990B418581487BB13A2CC67700A3C359804F91BDFB8E377CD0EC80DDC10",
                        "origin": "Embedded in certificate",
                        "signatureAlgorithm": "ECDSA",
                        "signatureData": "3046022100F319D0F56F27C82228E2B01934A1C7F46915A1509F094EE91508F08C3B5AE2B2022100B0D94DD6FD00CB435EC33B916B52EC76FE5FFCC5D5BD8CB559248243AEDFE3CE",
                        "status": "Verified",
                        "timestamp": 1511173770923.0 }],
                    "subjectName": "*.ipinyou.com",
                    "validFrom": 1511136000,
                    "validTo": 1547942399 },
                "securityState": "secure",
                "status": 302,
                "statusText": "Found",
                "timing": {
                    "connectEnd": 772.852999994939,
                    "connectStart": 0.566999995498918,
                    "dnsEnd": -1,
                    "dnsStart": -1,
                    "proxyEnd": -1,
                    "proxyStart": -1,
                    "pushEnd": 0,
                    "pushStart": 0,
                    "receiveHeadersEnd": 1226.29800000141,
                    "requestTime": 42129.997749,
                    "sendEnd": 773.012999998173,
                    "sendStart": 772.960999995121,
                    "sslEnd": 772.844999999506,
                    "sslStart": 1.62599999748636,
                    "workerReady": -1,
                    "workerStart": -1 },
                "url": "https://cm.ipinyou.com/baidu/cms.gif?baidu_error=1&timestamp=1525072001" },
            "request": {
                "headers": {
                    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36" },
                "initialPriority": "Low",
                "method": "GET",
                "mixedContentType": "none",
                "referrerPolicy": "no-referrer-when-downgrade",
                "url": "https://cm.g.doubleclick.net/pixel?google_nid=ipy&google_cm" },
            "requestId": "20524.247",
            "timestamp": 42131.225431,
            "type": "Image",
            "wallTime": 1525072000.35906 } },
    "webview": "28DAFE9FE90E9292F1B8EDB3315608EC" }
		
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117

参数说明:

参数 类型 说明
requestId String 唯一请求ID
loaderId String 加载ID
documentURL String 页面文档URL
request Request 请求数据对象
timestamp float 以过去某个任意时间点为基点,从打开页面开始,以秒为单位单调递增的时间戳
wallTime float UTC时间
initiator Initiator 请求初始化对象
redirectResponse Response 重定向响应对象
type String 资源类型
frameId String FrameID
hasUserGesture boolean Whether the request is initiated by a user gesture. Defaults to false.

其中, 
Request对象:

参数 类型 说明
url String 请求url
method String HTTP请求类型
headers Object 请求头信息
postData String Post请求数据
hasPostData boolean 如果是Post请求,则为true
mixedContentType String 是否存在混淆内容问题:blockable, optionally-blockable, none.
initialPriority String 资源加载优先级:VeryLow, Low, Medium, High, VeryHigh.
referrerPolicy String 跨域策略:no-referrer-when-downgrade, no-referrer, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin
isLinkPreload boolean 是否通过预加载方式加载

Response对象:

参数 类型 说明
url String 请求url
status int 响应状态码
statusText String 状态码内容
headers Object 响应头部,json格式
headersText String 响应头部,文本格式
mimeType String Resource mimeType
requestHeaders Obeject 请求头部,json格式
requestHeadersText String 请求头部,文本格式
connectionReused boolean 连接是否被复用
connectionId long 物理连接ID
remoteIPAddress String Remote IP address
remotePort int Remote port
fromDiskCache boolean 是否直接从浏览器缓存获取资源
fromServiceWorker boolean Specifies that the request was served from the ServiceWorker
encodedDataLength long 响应字节数
timing ResourceTiming ResourceTiming对象
protocol String 协议
securityState String Security state of the request resource:unknown, neutral, insecure, secure, info
securityDetails SecurityDetails Security details for the request

ResourceTiming对象:

参数 类型 说明
requestTime float 时间基线
proxyStart float Started resolving proxy.
proxyEnd float Finished resolving proxy.
dnsStart float Started DNS address resolve.
dnsEnd float Finished DNS address resolve.
connectStart float Started connecting to the remote host.
connectEnd float Connected to the remote host.
sslStart float Started SSL handshake.
sslEnd float Finished SSL handshake.
workerStart float Started running ServiceWorker.
workerReady float Finished Starting ServiceWorker.
sendStart float Started sending request.
sendEnd float Finished sending request.
pushStart float Time the server started pushing request.
pushEnd float Time the server finished pushing request.
receiveHeadersEnd float Finished receiving response headers.

Network.responseReceived

当HTTP响应可用时触发,其Json格式为:

{
    "message": {
        "method": "Network.responseReceived",
        "params": {
            "frameId": "28DAFE9FE90E9292F1B8EDB3315608EC",
            "loaderId": "44DBCD0BEBFCEE5AED6388366BCB719B",
            "requestId": "20524.277",
            "response": {
                "connectionId": 468,
                "connectionReused": true,
                "encodedDataLength": 439,
                "fromDiskCache": false,
                "fromServiceWorker": false,
                "headers": {
                    "Cache-Control": "no-cache, max-age=0, must-revalidate",
                    "Connection": "keep-alive",
                    "Content-Length": "43",
                    "Content-Type": "image/gif",
                    "Date": "Mon, 30 Apr 2018 07:06:42 GMT",
                    "Expires": "Fri, 01 Jan 1980 00:00:00 GMT",
                    "Last-Modified": "Mon, 28 Sep 1970 06:00:00 GMT",
                    "Pragma": "no-cache",
                    "Server": "nginx/1.6.3",
                    "X-Dscp-Value": "0",
                    "X-Via": "1.1 dxun38:1 (Cdn Cache Server V2.0), 1.1 shb115:4 (Cdn Cache Server V2.0), 1.1 ls10:0 (Cdn Cache Server V2.0)" },
                "headersText": "HTTP/1.1 200 OK\r\nDate: Mon, 30 Apr 2018 07:06:42 GMT\r\nServer: nginx/1.6.3\r\nContent-Type: image/gif\r\nContent-Length: 43\r\nLast-Modified: Mon, 28 Sep 1970 06:00:00 GMT\r\nExpires: Fri, 01 Jan 1980 00:00:00 GMT\r\nPragma: no-cache\r\nCache-Control: no-cache, max-age=0, must-revalidate\r\nX-Dscp-Value: 0\r\nX-Via: 1.1 dxun38:1 (Cdn Cache Server V2.0), 1.1 shb115:4 (Cdn Cache Server V2.0), 1.1 ls10:0 (Cdn Cache Server V2.0)\r\nConnection: keep-alive\r\n\r\n",
                "mimeType": "image/gif",
                "protocol": "http/1.1",
                "remoteIPAddress": "127.0.0.1",
                "remotePort": 1086,
                "requestHeaders": {
                    "Accept": "image/webp,image/apng,image/*,*/*;q=0.8",
                    "Accept-Encoding": "gzip, deflate, br",
                    "Accept-Language": "zh-CN,zh;q=0.9",
                    "Connection": "keep-alive",
                    "Cookie": "_snstyxuid=ADFD3F4299718846; _snvd=1525071994169