以查找元素为例,看 webdriver 的工作原理

本贴最后更新于 1604 天前,其中的信息可能已经时过境迁

selenium的历史

  1. selenium1.x:这个时候的selenium,使用的是JavaScript注入技术与浏览器打交道,需要Selenium RC启动一个Server,将操作Web元素的API调用转化为一段段Javascript,在Selenium内核启动浏览器之后注入这段Javascript。Javascript可以获取并调用DOM的任何元素,自如的进行操作。由此才实现了Selenium的目的:自动化Web操作。这种Javascript注入技术的缺点是速度不理想,而且稳定性大大依赖于Selenium内核对API翻译成的Javascript质量高低。
  2. selenium2.x:相比于selenium1.x,2.x版本整合了webdriver以及原版selenium,两个项目合二为一,虽然名字还叫selenium,但也可以叫Webdriver。这个版本的selenium是利用浏览器原生的API,封装成一套更加面向对象的Selenium WebDriver API,直接操作浏览器页面里的元素,甚至操作浏览器本身(截屏,窗口大小,启动,关闭,安装插件,配置证书之类的)。由于使用的是浏览器原生的API,速度大大提高,而且调用的稳定性交给了浏览器厂商本身,显然是更加科学。然而带来的一些副作用就是,不同的浏览器厂商,对Web元素的操作和呈现多少会有一些差异,这就直接导致了Selenium WebDriver要分浏览器厂商不同,而提供不同的实现。
    发展史请看下图:
    Alt

以下进入正题

结构

要通过selenium实现自动化测试,最最主要是需要三种东西。

代码

selenium支持多种语言(java/c#/python/ruby)。测试工程师通过编程语言,调用浏览器对应API实现需要的功能。

webdriver

webdriver,就像是一个媒介,代码驱动webdriver。上文提过,不同浏览器有不同的webdriver,例如火狐的FirefoxDriver,谷歌的 ChromeDriver.

浏览器

不同的浏览器对应不同的webdriver。

image.png

从上图,测试代码输入操作给webdriver,webdriver再去控制浏览器,最终达到的效果就是代码实现对浏览器的操作。

以查找元素为例,查看代码与webdriver的交互

以下python为例

from selenium import webdriver
driver = webdriver.Chrome()

这里driver是webdriver.Chrome()的对象,我们查看webdriver.Chrome()的源码,发现本质是
from .chrome.webdriver import WebDriver as Chrome 从目录名可知这来自chrome的webdriver,再次对这个WebDriver溯源,发现它是继承了一个RemoteWebDriver类,注释的含义是:控制ChromeDriver并允许驱动浏览器。

class WebDriver(RemoteWebDriver):
    """
    Controls the ChromeDriver and allows you to drive the browser.

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

    def __init__(...
...

再次对继承的RemoteWebDriver类溯源,发现其是selenium.webdriver.remote.webdriver.WebDriver

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

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

    _web_element_cls = WebElement

    def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub',
                 desired_capabilities=None, browser_profile=None, proxy=None,
                 keep_alive=False, file_detector=None, options=None):
...

注释的含义是:通过向远程服务器发送命令来控制浏览器。 该服务器应该运行WebDriver有线协议。这里先停一下,等会我们会再回来继续了解这个类。

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

ele = driver.find_element_by_id('id')

以上同方法,对代码溯源,find_elements_by_idselenium.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()**的实例方法的。

    def find_element(self, by=By.ID, value=None):
        """
        Find an element given a By strategy and locator. Prefer the find_element_by_* methods when
        possible.

        :Usage:
            element = driver.find_element(By.ID, 'foo')

        :rtype: WebElement
        """
        if self.w3c:
            if by == By.ID:
                by = By.CSS_SELECTOR
                value = '[id="%s"]' % value
            elif by == By.TAG_NAME:
                by = By.CSS_SELECTOR
            elif by == By.CLASS_NAME:
                by = By.CSS_SELECTOR
                value = ".%s" % value
            elif by == By.NAME:
                by = By.CSS_SELECTOR
                value = '[name="%s"]' % value
        return self.execute(Command.FIND_ELEMENT, {
            'using': by,
            'value': value})['value']

这个find_element方法最后调用了一个excute方法,我们再来看看这个excute方法:

    def execute(self, driver_command, params=None):
        """
        Sends a command to be executed by a command.CommandExecutor.
        发送一个命令由command.CommandExecutor执行。

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

        :Returns:
          The command's JSON response loaded into a dictionary object.
        """
        if self.session_id is not None:
            if not params:
                params = {'sessionId': self.session_id}
            elif 'sessionId' not in params:
                params['sessionId'] = self.session_id

        params = self._wrap_value(params)
        response = self.command_executor.execute(driver_command, params)
        if response:
            self.error_handler.check_response(response)
            response['value'] = self._unwrap_value(
                response.get('value', None))
            return response
        # If the server doesn't send a response, assume the command was
        # a success
        return {'success': 0, 'value': None, 'sessionId': self.session_id}

正如注释中提到的一样,其中的关键在于

response = self.command_executor.execute(driver_command, params)

一个名为command_executor的对象执行了execute方法。
名为command_executor的对象是RemoteConnection类的对象,并且这个对象是在新建selenium.webdriver.remote.webdriver.WebDriver类对象的时候就完成赋值的self.command_executor = RemoteConnection(command_executor, keep_alive=keep_alive)
结合selenium.webdriver.remote.webdriver.WebDriver类的类注释来看:WebDriver类的功能是通过给一个remote server发送指令来控制浏览器。而这个remote server是一个运行WebDriver wire protocolserver。而RemoteConnection类就是负责与Remote WebDriver server的连接的类。
可以注意到有这么一个新建WebDriver类的对象时候的参数command_executor,默认值='http://127.0.0.1:4444/wd/hub'。这个值表示的是访问remote server的URL。因此这个值作为了RemoteConnection类的构造方法的参数,因为要连接remote server,URL是必须的。
现在再来看RemoteConnection类的实例方法execute

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

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

        :Args:
         - command - A string specifying the command to execute.
         - params - A dictionary of named parameters to send with the command as
           its JSON payload.
        """
        command_info = self._commands[command]
        assert command_info is not None, 'Unrecognised command %s' % command
        data = utils.dump_json(params)
        path = string.Template(command_info[1]).substitute(params)
        url = '%s%s' % (self._url, path)
        return self._request(command_info[0], url, body=data)

这个方法有两个参数

command表示期望执行的指令的名字。打开self._commands这个dict,查看Command.FIND_ELEMENT的value.
指令的URL部分包含了几个组成部分:

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 Responsebody返回给测试代码,测试代码经过解析处理后得到想要的数据。

总结

image.png

初学者文中难免可能有疏漏之处,希望各位大佬指正

补充

为了怕同志们理解错,把雨泽大佬上课说的selenium工作原理写在下面,本菜鸟是在此基础上从另一角度出发来看:

1 回帖
请输入回帖内容 ...
  • yuze

    看源码的都是好同志,深度好文。😍