目录
6. 手工用Postman调用webdriver执行UI测试
Selemium是一款跨浏览器web应用程序自动化测试工具,其发展经历了三个阶段:Selenium Core、Selenium RC 和 Selenium WebDriver,现在每个浏览器都实现了 w3c WebDriver 接口,都有自己的驱动程序。
WebDriver 的开发人员倾向于向用户隐藏其并不关心的很多细节,提供尽可能简单的 API,好让用户聚焦在用例设计和发现 Bug 上。
WebDriver 协议 / WebDriver 规范 / WebDriver API在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请求,是不是也能控制浏览器呢?答案当然是肯定的。
不同的编程语言选择不同的Selenium客户端库。 对应我们Python语言来说,Selenium客户端库的安装非常简单,用 pip 命令即可。
打开 命令行程序,运行如下命令:
pip install selenium
如果安装不了,可能是网络问题,可以指定使用国内的豆瓣源
pip install selenium -i https://pypi.douban.com/simple/
浏览器驱动 是和 浏览器对应的。 不同的浏览器 需要选择不同的浏览器驱动。 https://www.google.cn/chrome/
https://chromedriver.storage.googleapis.com/index.html
下面的代码, 可以自动化的 打开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')
我们可以把浏览器驱动 所在目录 加入环境变量 Path ,
写代码时,就可以无需指定浏览器驱动路径了,
像下面这样: wd = webdriver.Chrome()
一定要注意的是, 加入环境变量 Path 的, 不是浏览器驱动全路径,比如 d:\tools\chromedriver.exe 而是 浏览器驱动所在目录,比如 d:\tools 而且设置完环境变量后,别忘了重启IDE(比如 PyCharm) 新的环境变量才会生效。
接下来我会以获取界面元素这个基本的操作为例来分析两者之间的关系。
在测试代码中,我们第一步要做的是新建一个webdriver类的对象:
wd = webdriver.Chrome()
这里新建的driver对象是一个webdriver.Chrome()类的对象,而webdriver.Chrome()类的本质是
from chrome.webdriver import WebDriver as Chrome
这一部分属于各个浏览器开发者和Webdriver开发者的范畴,所以我们不需要太关注,我们所关心的主要还是测试代码和Webdriver的关系,就好像出租车驾驶员如何驾驶汽车我们不需要关心一样。
# -*- 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
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
我正在使用i18n从头开始构建一个多语言网络应用程序,虽然我自己可以处理一大堆yml文件,但我说的语言(非常)有限,最终我想寻求外部帮助帮助。我想知道这里是否有人在使用UI插件/gem(与django上的django-rosetta不同)来处理多个翻译器,其中一些翻译器不愿意或无法处理存储库中的100多个文件,处理语言数据。谢谢&问候,安德拉斯(如果您已经在rubyonrails-talk上遇到了这个问题,我们深表歉意) 最佳答案 有一个rails3branchofthetolkgem在github上。您可以通过在Gemfi
我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass
我正在编写一个包含C扩展的gem。通常当我写一个gem时,我会遵循TDD的过程,我会写一个失败的规范,然后处理代码直到它通过,等等......在“ext/mygem/mygem.c”中我的C扩展和在gemspec的“扩展”中配置的有效extconf.rb,如何运行我的规范并仍然加载我的C扩展?当我更改C代码时,我需要采取哪些步骤来重新编译代码?这可能是个愚蠢的问题,但是从我的gem的开发源代码树中输入“bundleinstall”不会构建任何native扩展。当我手动运行rubyext/mygem/extconf.rb时,我确实得到了一个Makefile(在整个项目的根目录中),然后当
我有一个围绕一些对象的包装类,我想将这些对象用作散列中的键。包装对象和解包装对象应映射到相同的键。一个简单的例子是这样的:classAattr_reader:xdefinitialize(inner)@inner=innerenddefx;@inner.x;enddef==(other)@inner.x==other.xendenda=A.new(o)#oisjustanyobjectthatallowso.xb=A.new(o)h={a=>5}ph[a]#5ph[b]#nil,shouldbe5ph[o]#nil,shouldbe5我试过==、===、eq?并散列所有无济于事。
我有一些Ruby代码,如下所示:Something.createdo|x|x.foo=barend我想编写一个测试,它使用double代替block参数x,这样我就可以调用:x_double.should_receive(:foo).with("whatever").这可能吗? 最佳答案 specify'something'dox=doublex.should_receive(:foo=).with("whatever")Something.should_receive(:create).and_yield(x)#callthere
我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul
Sinatra新手;我正在运行一些rspec测试,但在日志中收到了一堆不需要的噪音。如何消除日志中过多的噪音?我仔细检查了环境是否设置为:test,这意味着记录器级别应设置为WARN而不是DEBUG。spec_helper:require"./app"require"sinatra"require"rspec"require"rack/test"require"database_cleaner"require"factory_girl"set:environment,:testFactoryGirl.definition_file_paths=%w{./factories./test/
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden