1. Selenium 概述
Selemium是一款跨浏览器web应用程序自动化测试工具,其发展经历了三个阶段:Selenium Core、Selenium RC 和 Selenium WebDriver,现在每个浏览器都实现了 w3c WebDriver 接口,都有自己的驱动程序。
WebDriver
的开发人员倾向于向用户隐藏其并不关心的很多细节,提供尽可能简单的 API,好让用户聚焦在用例设计和发现 Bug 上。
2. 术语解释:
- W3C WebDriver 是由 chrome WebDriver 发展而来的浏览器自动化协议,又称
WebDriver 协议
/WebDriver 规范
/WebDriver API
- Driver 是一个基于HTTP的协议,它提供了一系列的接口用于发现和控制 Web 文档中的 DOM 元素,几乎可以操作浏览器做任何事情。WebDriver协议的详细信息你可以在W3C WebDriver 规范中查看。比如 Chrome 浏览器的 ChromeDriver。
- ChromeDriver 是一个可以独立运行的服务器程序,它实现了 WebDriver 协议。
- Selenium WebDriver 是2009年8月由 Selenium 1.0 和 WebDriver 项目合并而成,遵循 w3c WebDriver 协议,早期又称作 Selenium 2.0 WebDriver。
3. Selenium WebDriver 实现原理
- 对于每一条Selenium脚本,一个http请求会被创建并且发送给浏览器的驱动
- 浏览器驱动中包含了一个HTTP Server,用来接收这些http请求
- HTTP Server接收到请求后根据请求来具体操控对应的浏览器
- 浏览器执行具体的测试步骤
- 浏览器将步骤执行结果返回给HTTP Server
- HTTP Server又将结果返回给Selenium的脚本
在Selenium3之前WebDriver基于的协议:JSON Wire protocol JSON Wire protocol是在http协议基础上,对http请求及响应的body部分的数据的进一步规范。body部分主要传送具体的数据,在WebDriver中这些数据都是以JSON的形式存在并进行传送的,这就是JSON Wire protocol。
Selenium3和Selenium4的最大不同,就是Selenium4放弃了JSON Wire Protocol,转而直接通过W3c web Protocol与各浏览器的Driver进行交互。在早期,浏览器的Driver都是由Selenium来提供的,浏览器的访问接口也并不统一,所以需要使用JSON Wire Protocol来负责在Selenium Client和各Browser Driver之间传递数据。而在Selenium4阶段,浏览器的访问接口都遵守W3c web Protocol,并且各浏览器自己维护各自的Driver。一句话,浏览器自己的驱动更加标准化了,从而提高了selenium代码的效率和稳定性。
以访问百度为例,代码实现为driver.get('https://www.baidu.com/'),查看源码,可以在webdriver.py中看到get方法实际上是调用了self.execute(Command.GET, {'url': url}),而WebDriver类是继承自Remoteexecute方法的核心逻辑是response = self.command_executor.execute(driver_command, params),其中Command.GET在remote_connection.py中的定义为Command.GET: ('POST', '/session/$sessionId/url')
也就是说,Selenium测试代码运行时,会向浏览器driver发送对应的HTTP请求,浏览器driver在收到请求后对浏览器进行操作,再将结果返回给Selenium测试代码。那么,如果我们先运行浏览器driver,再通过Postman向其发送HTTP请求,是不是也能控制浏览器呢?答案当然是肯定的。
4. 安装selenium 客户端,浏览器,驱动
4.1 安装selenium client lib
不同的编程语言选择不同的Selenium客户端库。 对应我们Python语言来说,Selenium客户端库的安装非常简单,用 pip 命令即可。
打开 命令行程序,运行如下命令:
pip install selenium
如果安装不了,可能是网络问题,可以指定使用国内的豆瓣源
pip install selenium -i https://pypi.douban.com/simple/
4.2 安装浏览器和浏览器驱动
浏览器驱动 是和 浏览器对应的。 不同的浏览器 需要选择不同的浏览器驱动。 https://www.google.cn/chrome/
https://chromedriver.storage.googleapis.com/index.html
4.3 例子代码
下面的代码, 可以自动化的 打开Chrome浏览器,并且自动化打开百度网站,可以大家可以运行一下看看。
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
创建 WebDriver 对象,指明使用chrome浏览器驱动
wd = webdriver.Chrome(service=Service(r'd:\tools\chromedriver.exe'))
调用WebDriver 对象的get方法 可以让浏览器打开指定网址
wd.get('https://www.baidu.com')
4.4 省略浏览器驱动的方法
我们可以把浏览器驱动 所在目录 加入环境变量 Path ,
写代码时,就可以无需指定浏览器驱动路径了,
像下面这样: wd = webdriver.Chrome()
一定要注意的是, 加入环境变量 Path 的, 不是浏览器驱动全路径,比如 d:\tools\chromedriver.exe 而是 浏览器驱动所在目录,比如 d:\tools 而且设置完环境变量后,别忘了重启IDE(比如 PyCharm) 新的环境变量才会生效。
**4.5 **测试代码与Webdriver的交互
接下来我会以获取界面元素这个基本的操作为例来分析两者之间的关系。
在测试代码中,我们第一步要做的是新建一个webdriver类的对象:
wd = webdriver.Chrome()
这里新建的driver对象是一个webdriver.Chrome()类的对象,而webdriver.Chrome()类的本质是
from chrome.webdriver import WebDriver as Chrome
5. Webdriver与浏览器的关系
这一部分属于各个浏览器开发者和Webdriver开发者的范畴,所以我们不需要太关注,我们所关心的主要还是测试代码和Webdriver的关系,就好像出租车驾驶员如何驾驶汽车我们不需要关心一样。
6. 手工用Postman调用webdriver执行UI测试
# -*- coding: utf-8 -*-
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
chrome_driver_path = ChromeDriverManager().install()
service = Service(executable_path=chrome_driver_path)
option = webdriver.ChromeOptions()
driver = webdriver.Chrome(service=service, options=option)
driver.get("https://www.baidu.com/")
input_box = driver.find_element(By.ID, "kw")
input_box.send_keys("Hello")
input_box.send_keys(Keys.ENTER)
driver.quit()
以前用过selenium的人,可能都知道需要下载与浏览器版本对应的driver,要不然你的selenium代码就会罢工。最要命的是,如果你不去特意设置,我们的浏览器都是会自动更新版本的,这就让driver不匹配的问题防不胜防。而到了selenium4时代,引入了ChromeDriverManager(pip install webdriver-manager),从而可以根据我们的浏览器当前版本,在代码运行时下载对应的Chrome Driver。其它浏览器也支持这样的功能。当然,ChromeDriverManager并不是我们今天要聊的重点。
我们今天的重点是,手动模拟Selenium Client和Chrome Driver之间的交互。
第一步: 启动Chrome Driver服务
命令行执行命令: chromedriver -port=57789
Chrome driver使用的端口其实是随机的。出于测试目的,此处使用57789。如此,HTTP API的host就是localhost:57789
第二步: 创建一个新session
打开Postman,发送http post request:
此处,API的协议为HTTP,Method是POST,HOST是"localhost:577789", 而Path是"/session"
ChromeDriver在收到这个request后,就会将指令发送给与Driver对应的浏览器,一个新的浏览器实例会被启动,并返回创建好的session信息,此处,sessionID至关重要,因为在后面对浏览器的操作中必须用到。
第三步: 导航到指定的网址 -> 对应代码driver.get("https://www.baidu.com/")
打开Postman,发送http post request:
此处,API的协议为HTTP,Method是POST,HOST是"localhost:577789", 而Path是"/session/{{sessionId}}/url"
ChromeDriver在收到这个request后,就会将指令传达给Chrome浏览器;浏览器收到指令后,在刚打开的浏览器实例上访问上传的URL,然后返回执行结果给Driver;最后Driver返回指令执行结果。为了确保指令传输给了正确的浏览器,API的endpoint中包含了第二步所创建的sessionID。
第四步:find element -> 对应代码driver.find_element(By.ID, "kw")
打开Postman,发送http post request:
此处,API的协议为HTTP,Method是POST,HOST是"localhost:577789", 而Path是"/session/{{sessionId}}/element"
ChromeDriver在收到这个request后,就会将指令传达给Chrome浏览器;浏览器收到指令后,在刚打开页面根据指令查找指定的元素,然后返回查找结果给Driver;最后Driver返回指令执行结果。这边我们会获取到element的ID。这将会在后面作为这个元素的唯一标示,直到session结束。
第五步:输入文字"Hello" -> 对应代码input_box.send_keys("Hello")
打开Postman,发送http post request:
此处,API的协议为HTTP,Method是POST,HOST是"localhost:577789", 而Path是"/session/{{sessionId}}/element/{{elementId}}/value"
ChromeDriver在收到这个request后,就会将指令传达给Chrome浏览器;浏览器收到指令后在刚打开页面根据elementID找到指定的元素,并输入指定的文字,然后返回执行结果给Driver;最后Driver返回指令执行结果。
第六步: 关闭session -> 对应代码driver.quit()
打开Postman,发送http delete request:
此处,API的协议为HTTP,Method是DELETE,HOST是"localhost:577789", 而Path是"/session/{{sessionId}}"
ChromeDriver在收到这个request后,就会将指令传达给Chrome浏览器;浏览器会关闭刚才打开的实例并返回结果给Driver;Driver会删除刚才创建的session,然后返回结果。
通过上面的步骤,相信你对Selenium Client,Browser driver和browser之间的交互会有一些更加直观的认识。或者拓展了思路也未可知。
最后,贴下Selenium命令和HTTP Rest API的对应关系:
{
Command.NEW_SESSION: ('POST', '/session'),
Command.QUIT: ('DELETE', '/session/$sessionId'),
Command.W3C_GET_CURRENT_WINDOW_HANDLE:
('GET', '/session/$sessionId/window'),
Command.W3C_GET_WINDOW_HANDLES:
('GET', '/session/$sessionId/window/handles'),
Command.GET: ('POST', '/session/$sessionId/url'),
Command.GO_FORWARD: ('POST', '/session/$sessionId/forward'),
Command.GO_BACK: ('POST', '/session/$sessionId/back'),
Command.REFRESH: ('POST', '/session/$sessionId/refresh'),
Command.W3C_EXECUTE_SCRIPT:
('POST', '/session/$sessionId/execute/sync'),
Command.W3C_EXECUTE_SCRIPT_ASYNC:
('POST', '/session/$sessionId/execute/async'),
Command.GET_CURRENT_URL: ('GET', '/session/$sessionId/url'),
Command.GET_TITLE: ('GET', '/session/$sessionId/title'),
Command.GET_PAGE_SOURCE: ('GET', '/session/$sessionId/source'),
Command.SCREENSHOT: ('GET', '/session/$sessionId/screenshot'),
Command.ELEMENT_SCREENSHOT: ('GET', '/session/$sessionId/element/$id/screenshot'),
Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'),
Command.FIND_ELEMENTS: ('POST', '/session/$sessionId/elements'),
Command.W3C_GET_ACTIVE_ELEMENT: ('GET', '/session/$sessionId/element/active'),
Command.FIND_CHILD_ELEMENT:
('POST', '/session/$sessionId/element/$id/element'),
Command.FIND_CHILD_ELEMENTS:
('POST', '/session/$sessionId/element/$id/elements'),
Command.CLICK_ELEMENT: ('POST', '/session/$sessionId/element/$id/click'),
Command.CLEAR_ELEMENT: ('POST', '/session/$sessionId/element/$id/clear'),
Command.GET_ELEMENT_TEXT: ('GET', '/session/$sessionId/element/$id/text'),
Command.SEND_KEYS_TO_ELEMENT:
('POST', '/session/$sessionId/element/$id/value'),
Command.UPLOAD_FILE: ('POST', "/session/$sessionId/se/file"),
Command.GET_ELEMENT_TAG_NAME:
('GET', '/session/$sessionId/element/$id/name'),
Command.IS_ELEMENT_SELECTED:
('GET', '/session/$sessionId/element/$id/selected'),
Command.IS_ELEMENT_ENABLED:
('GET', '/session/$sessionId/element/$id/enabled'),
Command.GET_ELEMENT_RECT:
('GET', '/session/$sessionId/element/$id/rect'),
Command.GET_ELEMENT_ATTRIBUTE:
('GET', '/session/$sessionId/element/$id/attribute/$name'),
Command.GET_ELEMENT_PROPERTY:
('GET', '/session/$sessionId/element/$id/property/$name'),
Command.GET_ELEMENT_ARIA_ROLE:
('GET', '/session/$sessionId/element/$id/computedrole'),
Command.GET_ELEMENT_ARIA_LABEL:
('GET', '/session/$sessionId/element/$id/computedlabel'),
Command.GET_SHADOW_ROOT:
('GET', '/session/$sessionId/element/$id/shadow'),
Command.FIND_ELEMENT_FROM_SHADOW_ROOT:
('POST', '/session/$sessionId/shadow/$shadowId/element'),
Command.FIND_ELEMENTS_FROM_SHADOW_ROOT:
('POST', '/session/$sessionId/shadow/$shadowId/elements'),
Command.GET_ALL_COOKIES: ('GET', '/session/$sessionId/cookie'),
Command.ADD_COOKIE: ('POST', '/session/$sessionId/cookie'),
Command.GET_COOKIE: ('GET', '/session/$sessionId/cookie/$name'),
Command.DELETE_ALL_COOKIES:
('DELETE', '/session/$sessionId/cookie'),
Command.DELETE_COOKIE:
('DELETE', '/session/$sessionId/cookie/$name'),
Command.SWITCH_TO_FRAME: ('POST', '/session/$sessionId/frame'),
Command.SWITCH_TO_PARENT_FRAME: ('POST', '/session/$sessionId/frame/parent'),
Command.SWITCH_TO_WINDOW: ('POST', '/session/$sessionId/window'),
Command.NEW_WINDOW: ('POST', '/session/$sessionId/window/new'),
Command.CLOSE: ('DELETE', '/session/$sessionId/window'),
Command.GET_ELEMENT_VALUE_OF_CSS_PROPERTY:
('GET', '/session/$sessionId/element/$id/css/$propertyName'),
Command.EXECUTE_ASYNC_SCRIPT: ('POST', '/session/$sessionId/execute_async'),
Command.SET_TIMEOUTS:
('POST', '/session/$sessionId/timeouts'),
Command.GET_TIMEOUTS:
('GET', '/session/$sessionId/timeouts'),
Command.W3C_DISMISS_ALERT:
('POST', '/session/$sessionId/alert/dismiss'),
Command.W3C_ACCEPT_ALERT:
('POST', '/session/$sessionId/alert/accept'),
Command.W3C_SET_ALERT_VALUE:
('POST', '/session/$sessionId/alert/text'),
Command.W3C_GET_ALERT_TEXT:
('GET', '/session/$sessionId/alert/text'),
Command.W3C_ACTIONS:
('POST', '/session/$sessionId/actions'),
Command.W3C_CLEAR_ACTIONS:
('DELETE', '/session/$sessionId/actions'),
Command.SET_WINDOW_RECT:
('POST', '/session/$sessionId/window/rect'),
Command.GET_WINDOW_RECT:
('GET', '/session/$sessionId/window/rect'),
Command.W3C_MAXIMIZE_WINDOW:
('POST', '/session/$sessionId/window/maximize'),
Command.SET_SCREEN_ORIENTATION:
('POST', '/session/$sessionId/orientation'),
Command.GET_SCREEN_ORIENTATION:
('GET', '/session/$sessionId/orientation'),
Command.GET_NETWORK_CONNECTION:
('GET', '/session/$sessionId/network_connection'),
Command.SET_NETWORK_CONNECTION:
('POST', '/session/$sessionId/network_connection'),
Command.GET_LOG:
('POST', '/session/$sessionId/se/log'),
Command.GET_AVAILABLE_LOG_TYPES:
('GET', '/session/$sessionId/se/log/types'),
Command.CURRENT_CONTEXT_HANDLE:
('GET', '/session/$sessionId/context'),
Command.CONTEXT_HANDLES:
('GET', '/session/$sessionId/contexts'),
Command.SWITCH_TO_CONTEXT:
('POST', '/session/$sessionId/context'),
Command.FULLSCREEN_WINDOW:
('POST', '/session/$sessionId/window/fullscreen'),
Command.MINIMIZE_WINDOW:
('POST', '/session/$sessionId/window/minimize'),
Command.PRINT_PAGE:
('POST', '/session/$sessionId/print'),
Command.ADD_VIRTUAL_AUTHENTICATOR:
('POST', '/session/$sessionId/webauthn/authenticator'),
Command.REMOVE_VIRTUAL_AUTHENTICATOR:
('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId'),
Command.ADD_CREDENTIAL:
('POST', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credential'),
Command.GET_CREDENTIALS:
('GET', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials'),
Command.REMOVE_CREDENTIAL:
('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials/$credentialId'),
Command.REMOVE_ALL_CREDENTIALS:
('DELETE', '/session/$sessionId/webauthn/authenticator/$authenticatorId/credentials'),
Command.SET_USER_VERIFIED:
('POST', '/session/$sessionId/webauthn/authenticator/$authenticatorId/uv'),
}
参考:Postman模拟Selenium Client和Web driver的交互_Lemon2050的博客-CSDN博客_seleniumide结合 postman
版权归原作者 全栈开发与测试 所有, 如有侵权,请联系我们删除。