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

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

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