51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 2261|回复: 0
打印 上一主题 下一主题

关于Selenium Webdriver 实现原理的一点思考和分享

[复制链接]
  • TA的每日心情
    奋斗
    2021-8-16 14:04
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]测试小兵

    跳转到指定楼层
    1#
    发表于 2018-4-20 13:16:44 | 只看该作者 回帖奖励 |正序浏览 |阅读模式
    作为一名使用Selenium开发UI自动化多年的工程师,一直都对Selenium Webdriver的实现原理感觉不是很清楚。
    怎么就通过脚本控制浏览器进行各种操作了呢?相信很多Selenium的使用者也会有类似的疑惑。最近针对这个
    问题看了不少了文章和书籍,在加上一点自己的思考和整理,与大家一起分享,一起学习。文章中如果有不准
    确的地方,希望大家给予指正。

    结构

    想要使用Selenium实现自动化测试,主要需要三个东西。

    测试代码
    Webdriver
    浏览器
    测试代码

    测试代码就是程序员利用不同的语言和相应的selenium API库完成的代码。本文将以python为例进行说明。

    Webdriver

    Webdriver是针对不同的浏览器开发的,不同的浏览器有不同的webdriver。例如针对Chrome使用的chromed
    river。

    浏览器

    浏览器和相应的Webdriver对应。

    首先我们来看一下这三个部分的关系。
    对于三个部分的关系模型,可以用一个日常生活中常见的例子来类比。


    关系模型
    对于打的这个行为来说,乘客和出租车司机进行交互,告诉出租车想去的目的地,出租车司机驾驶汽车把乘
    客送到目的地,这样乘客就乘坐出租车到达了自己想去的地方。
    这和Webdriver的实现原理是类似的,测试代码中包含了各种期望的对浏览器界面的操作,例如点击。测试
    代码通过给Webdriver发送指令,让Webdriver知道想要做的操作,而Webdriver根据这些操作在浏览器界面
    上进行控制,由此测试代码达到了在浏览器界面上操作的目的。
    理清了Selenium自动化测试三个重要组成之间的关系,接下来我们来具体分析其中一个最重要的关系。

    测试代码与Webdriver的交互

    接下来我会以获取界面元素这个基本的操作为例来分析两者之间的关系。
    在测试代码中,我们第一步要做的是新建一个webdriver类的对象:

    1. from selenium import webdriver
    2. driver = webdriver.Chrome()
    3. 这里新建的driver对象是一个webdriver.Chrome()类的对象,而webdriver.Chrome()类的本质是

    4. from .chrome.webdriver import WebDriver as Chrome
    5. 也就是一个来自chrome的WebDriver类。这个.chrome.webdriver.WebDriver是继承了selenium.webdriver.rem
    6. ote.webdriver.WebDriver

    7. from selenium.webdriver.remote.webdriver import WebDriver as RemoteWebDriver
    8. ...
    9. class WebDriver(RemoteWebDriver):
    10.     """
    11.     Controls the ChromeDriver and allows you to drive the browser.

    12.     You will need to download the ChromeDriver executable from
    13.     http://chromedriver.storage.googleapis.com/index.html
    14.     """

    15.     def __init__(self, executable_path="chromedriver", port=0,
    16.                  chrome_options=None, service_args=None,
    17.                  desired_capabilities=None, service_log_path=None):
    复制代码

    ...
    以python为例,在selenium库中,通过ID获取界面元素的方法是这样的:

    from selenium import webdriver
    driver = webdriver.Chrome()
    driver.find_element_by_id(id)
    find_elements_by_id是selenium.webdriver.remote.webdriver.WebDriver类的实例方法。在代码中,我们直接
    使用的其实不是selenium.webdriver.remote.webdriver.WebDriver这个类,而是针对各个浏览器的webdriver类,
    例如webdriver.Chrome()。
    所以说在测试代码中执行各种浏览器操作的方法其实都是selenium.webdriver.remote.webdriver.WebDriver类
    的实例方法。
    接下来我们再深入selenium.webdriver.remote.webdriver.WebDriver类来看看具体是如何实现例如find_element
    _by_id()的实例方法的。
    通过Source code可以看到:

    1.     def find_element(self, by=By.ID, value=None):
    2.         """
    3.         'Private' method used by the find_element_by_* methods.

    4.         :Usage:
    5.             Use the corresponding find_element_by_* instead of this.

    6.         :rtype: WebElement
    7.         """
    8.         if self.w3c:
    9.       ...
    10.         return self.execute(Command.FIND_ELEMENT, {
    11.             'using': by,
    12.             'value': value})['value']
    13. 这个方法最后call了一个execute方法,方法的定义如下:

    14.     def execute(self, driver_command, params=None):
    15.         """
    16.         Sends a command to be executed by a command.CommandExecutor.

    17.         :Args:
    18.          - driver_command: The name of the command to execute as a string.
    19.          - params: A dictionary of named parameters to send with the command.

    20.         :Returns:
    21.           The command's JSON response loaded into a dictionary object.
    22.         """
    23.         if self.session_id is not None:
    24.             if not params:
    25.                 params = {'sessionId': self.session_id}
    26.             elif 'sessionId' not in params:
    27.                 params['sessionId'] = self.session_id

    28.         params = self._wrap_value(params)
    29.         response = self.command_executor.execute(driver_command, params)
    30.         if response:
    31.             self.error_handler.check_response(response)
    32.             response['value'] = self._unwrap_value(
    33.                 response.get('value', None))
    34.             return response
    35.         # If the server doesn't send a response, assume the command was
    36.         # a success
    37.         return {'success': 0, 'value': None, 'sessionId': self.session_id}
    38. 正如注释中提到的一样,其中的关键在于

    39. response = self.command_executor.execute(driver_command, params)
    40. 一个名为command_executor的对象执行了execute方法。
    41. 名为command_executor的对象是RemoteConnection类的对象,并且这个对象是在新建selenium.webdriver.re
    42. mote.webdriver.WebDriver类对象的时候就完成赋值的self.command_executor = RemoteConnection(comma
    43. nd_executor, keep_alive=keep_alive)。
    44. 结合selenium.webdriver.remote.webdriver.WebDriver类的类注释来看:

    45. class WebDriver(object):
    46.     """
    47.     Controls a browser by sending commands to a remote server.
    48.     This server is expected to be running the WebDriver wire protocol
    49.     as defined at
    50.     https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol

    51.     :Attributes:
    52.      - session_id - String ID of the browser session started and controlled by this WebDriver.
    53.      - capabilities - Dictionaty of effective capabilities of this browser session as returned
    54.          by the remote server. See https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities
    55.      - command_executor - remote_connection.RemoteConnection object used to execute commands.
    56.      - error_handler - errorhandler.ErrorHandler object used to handle errors.
    57.     """

    58.     _web_element_cls = WebElement

    59.     def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
    60.                  desired_capabilities=None, browser_profile=None, proxy=None,
    61.                  keep_alive=False, file_detector=None):
    复制代码

    WebDriver类的功能是通过给一个remote server发送指令来控制浏览器。而这个remote server是一个运行Web
    Driver wire protocol的server。而RemoteConnection类就是负责与Remote WebDriver server的连接的类。
    可以注意到有这么一个新建WebDriver类的对象时候的参数command_executor,默认值='http://127.0.0.1:4
    444/wd/hub'。这个值表示的是访问remote server的URL。因此这个值作为了RemoteConnection类的构造方法
    的参数,因为要连接remote server,URL是必须的。
    现在再来看RemoteConnection类的实例方法execute。

    1.     def execute(self, command, params):
    2.         """
    3.         Send a command to the remote server.

    4.         Any path subtitutions required for the URL mapped to the command should be
    5.         included in the command parameters.

    6.         :Args:
    7.          - command - A string specifying the command to execute.
    8.          - params - A dictionary of named parameters to send with the command as
    9.            its JSON payload.
    10.         """
    11.         command_info = self._commands[command]
    12.         assert command_info is not None, 'Unrecognised command %s' % command
    13.         data = utils.dump_json(params)
    14.         path = string.Template(command_info[1]).substitute(params)
    15.         url = '%s%s' % (self._url, path)
    16.         return self._request(command_info[0], url, body=data)
    复制代码

    这个方法有两个参数:

    command
    params
    command表示期望执行的指令的名字。通过观察self._commands这个dict可以看到,self._commands存储了se
    lenium.webdriver.remote.command.Command类里的常量指令和WebDriver wire protocol中定义的指令的对应
    关系。

    1. self._commands = {
    2.             Command.STATUS: ('GET', '/status'),
    3.             Command.NEW_SESSION: ('POST', '/session'),
    4.             Command.GET_ALL_SESSIONS: ('GET', '/sessions'),
    5.             Command.QUIT: ('DELETE', '/session/$sessionId'),
    6. ...
    7.             Command.FIND_ELEMENT: ('POST', '/session/$sessionId/element'),
    8. 以FIND_ELEMENT为例可以看到,指令的URL部分包含了几个组成部分:
    复制代码


    HTTP请求方法。WebDriver wire protocol中定义的指令是符合RESTful规范的,通过不同请求方法对应不同的
    指令操作。

    sessionId。Session的概念是这么定义的:

    The server should maintain one browser per session. Commands sent to a session will be directed to the corresp
    onding browser.
    也就是说sessionId表示了remote server和浏览器的一个会话,指令通过这个会话变成对于浏览器的一个操作。

    element。这一部分用来表示具体的指令。

    而selenium.webdriver.remote.command.Command类里的常量指令又在各个具体的类似find_elements的实例方
    法中作为execute方法的参数来使用,这样就实现了selenium.webdriver.remote.webdriver.WebDriver类中实现
    各种操作的实例方法与WebDriver wire protocol中定义的指令的一一对应。
    而selenium.webdriver.remote.webelement.WebElement中各种在WebElement上的操作也是用类似的原理实现的。

    实例方法execute的另一个参数params则是用来保存指令的参数的,这个参数将转化为JSON格式,作为HTTP请
    求的body发送到remote server。
    remote server在执行完对浏览器的操作后得到的数据将作为HTTP Response的body返回给测试代码,测试代码
    经过解析处理后得到想要的数据。

    Webdriver与浏览器的关系

    这一部分属于各个浏览器开发者和Webdriver开发者的范畴,所以我们不需要太关注,我们所关心的主要还是测
    试代码和Webdriver的关系,就好像出租车驾驶员如何驾驶汽车我们不需要关心一样。

    总结

    关系

    最后通过这个关系图来简单的描述Selenium三个组成部分的关系。通过对python selenium库的分析,希望能够帮
    助大家对selenium和webdriver的实现原理有更进一步的了解,在日常的自动化脚本开发中更加快捷的定位问题
    和解决问题。



    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏1
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-11-23 08:29 , Processed in 0.068672 second(s), 25 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表