pychon - selenium2Libray源码简介
I. Introduction
Selenium2Library是robot framework中主流的测试网页功能的库, 它的本质是对webdriver的二次封装, 以适应robot框架. 百度上一堆Selenium2Library的介绍, 这里不再炒剩饭. 但是源码分析的资料, 少之又少. 所以本文就从源码角度介绍Selenium2Library. 一方面能够了解robot framework是如何对原有库或驱动进行二次封装, 另一方面能加强对python的了解和使用.
Selenium2Library包括4个package:
- keywords
- locators
- resources
- utils
keywords 即关键字集; locators从字面上理解是定位, 从监测到网页的元素到应该执行的方法之间也需要"定位", 通过locator作为桥梁, 传递方法\属性\值; resources里只放了和firefox文件相关的资源; utils里是一些基本的web初始化操作, 如打开浏览器, 关闭浏览器. 最后整个Library还调用了WebDriver的驱动, 同时获得了WebDriver提供的Monkey Patch功能.
II. Module: keywords
所有关键字封装, 还包含了对关键字的"技能加成":
- __init__.py
- _browsermanagement.py
- _cookie.py
- _element.py
- _formelement.py
- _javascript.py
- _logging.py
- _runonfailure.py 运行失败的异常处理封装
- _screenshot.py
- _selectelement.py
- _tableelement.py
- _waiting.py 各种条件的等待
- keywordgroup.py
2.1 __init__.py
每个module都需要__init__.py文件,用于启动, 配置, 描述对外接口. 这里只把keywords的__init__.py拿出来解读:
1 # from modula import * 表示默认导入modula中所有不以下划线开头的成员 2 from _logging import _LoggingKeywords 3 from _runonfailure import _RunOnFailureKeywords 4 from _browsermanagement import _BrowserManagementKeywords 5 from _element import _ElementKeywords 6 from _tableelement import _TableElementKeywords 7 from _formelement import _FormElementKeywords 8 from _selectelement import _SelectElementKeywords 9 from _javascript import _JavaScriptKeywords 10 from _cookie import _CookieKeywords 11 from _screenshot import _ScreenshotKeywords 12 from _waiting import _WaitingKeywords 13 14 # 定义了__all__后, 表示只导出以下列表中的成员 15 __all__ = [ 16 "_LoggingKeywords", 17 "_RunOnFailureKeywords", 18 "_BrowserManagementKeywords", 19 "_ElementKeywords", 20 "_TableElementKeywords", 21 "_FormElementKeywords", 22 "_SelectElementKeywords", 23 "_JavaScriptKeywords", 24 "_CookieKeywords", 25 "_ScreenshotKeywords", 26 "_WaitingKeywords" 27 ]
2.2 _waiting.py
每个wait函数, 除了wait条件不同, 需要调用self.***函数进行前置条件判断外, 最终实现都是调用该类两个内部函数之一的_wait_until_no_error(self, timeout, wait_func, *args). 而含有条件的等待, 相比sleep函数在每次执行后强制等待固定时间, 可以有效节省执行时间, 也能尽早抛出异常
_wait_until_no_error(self, timeout, wait_func, *args)
说明: 等待, 直到传入的函数wait_func(*args)有返回, 或者超时. 底层实现, 逻辑覆盖完全, 参数最齐全, 最抽象.
参数:
timeout: 超时时间
wait_func: 函数作为参数传递
返回:
None\error(Timeout or 其它)
1 def _wait_until_no_error(self, timeout, wait_func, *args): 2 timeout = robot.utils.timestr_to_secs(timeout) if timeout is not None else self._timeout_in_secs 3 maxtime = time.time() + timeout 4 while True: 5 timeout_error = wait_func(*args) 6 if not timeout_error: return #如果wait_func()无返回,进行超时判断;否则返回wait_func()执行结果 7 if time.time() > maxtime: #超时强制抛出timeout_error异常 8 raise AssertionError(timeout_error) 9 time.sleep(0.2)
_wait_until(self, timeout, error, function, *args)
说明: 等待, 直到传入的函数function(*args)有返回, 或者超时
参数:
error: 初始化为超时异常, 是对_wait_until_no_error的又一层功能删减版封装, 使得error有且仅有一种error: timeout
function: 条件判断, 返回True or False
返回:
None\error(Timeout)
1 def _wait_until(self, timeout, error, function, *args): 2 error = error.replace('<TIMEOUT>', self._format_timeout(timeout)) 3 def wait_func(): 4 return None if function(*args) else error 5 self._wait_until_no_error(timeout, wait_func)
wait_for_condition(self, condition, timout=None, error=None)
说明: 等待, 直到满足condition条件或者超时
备注: 传入函数参数时使用了python的lambda语法, 简单来说就是函数定义的代码简化版. 知乎上有篇关于Lambda的Q&A非常棒:Lambda表达式有何用处?如何使用?-Python-知乎. 所以分析源码时又插入了抽象函数逻辑, 高阶函数学习的小插曲, 脑补了Syntanic Sugar, 真是...抓不住西瓜芝麻掉一地...
1 def wait_for_condition(self, condition, timeout=None, error=None): 2 if not error: 3 error = "Condition '%s' did not become true in <TIMEOUT>" % condition 4 self._wait_until(timeout, error, lambda: self._current_browser().execute_script(condition) == True)
调用条件判断self._is_text_present(text)系列
wait_until_page_contains(self, text, timeout=None, error=None)
wait_until_page_does_not_contain(self, text, timeout=None, error=None)
调用条件判断self._is_element_present(locator)系列
wait_until_page_contains_element(self, locator, timeout=None, error=None)
wait_until_page_does_not_contain_element(self, locator, timeout=None, error=None)
调用条件判断self._is_visible(locator)系列
wait_until_element_is_visible(self, locator, timeout=None, error=None)
wait_until_element_is_not_visible(self, locator, timeout=None, error=None)
调用条件判断self._element_find(locator, True, True)系列
wait_until_element_is_enabled(self, locator, timeout=None, error=None)
wait_until_element_contains(self, locator, text, timeout=None, error=None)
wait_until_element_does_not_contain(self, locator, text, timeout=None, error=None)
2.3 keywordgroup.py
keywordgroup里的两个类和一个内部方法很好理解, 就是为每个关键字加上 _run_on_failure_decorator 的"技能", 用到了python的decorator语法, 这也是继上文的lambda语法糖后遇到的另一种小技巧.
和默认传统类的类型不同, KeywordGroup的元类属性被重定义为KeywordGroupMetaClass, 为什么要重新定义元类? 看源码可以发现, 元类的构造器被重定义了, 所有该类的派生对象都会在构造时判断是否添加_run_on_failure_decorator 方法:
1 class KeywordGroupMetaClass(type): 2 def __new__(cls, clsname, bases, dict): 3 if decorator: 4 for name, method in dict.items(): 5 if not name.startswith('_') and inspect.isroutine(method): 6 dict[name] = decorator(_run_on_failure_decorator, method) 7 return type.__new__(cls, clsname, bases, dict)
在keywordgroup.py里为每个传入的关键字加上了decorator, 那么这些关键字在定义后又是如何传入keywordgroup的类构造器中的呢? _runonfailure.py中给出了答案:
1 def register_keyword_to_run_on_failure(self, keyword): 2 old_keyword = self._run_on_failure_keyword 3 old_keyword_text = old_keyword if old_keyword is not None else "No keyword" 4 5 new_keyword = keyword if keyword.strip().lower() != "nothing" else None 6 new_keyword_text = new_keyword if new_keyword is not None else "No keyword" 7 8 self._run_on_failure_keyword = new_keyword 9 self._info('%s will be run on failure.' % new_keyword_text) 10 11 return old_keyword_text
上面的代码作用是当一个Selenium2Library中的关键字执行失败后, 执行指定的keyword. 默认失败后执行"Capture Page Screenshot".
III. Module: locators
Selenium中提供了多种元素定位策略, 在locators中实现. 其实这些定位方法都是对WebDriver的元素定位接口的封装.
3.1 elementfinder.py
Web元素定位有很多策略, 如通过id, name, xpath等属性定位, 这些不同定位策略的最终实现是通过find(self, browser, locator, tag=None)方法. 传入浏览器对象browser和定位元素对象locator, 通过解析locator, 得到两个信息: 前缀prefix, 定位规则criteria. 前缀即不同策略, 但是解析前缀的这句strategy = self._strategies.get(prefix)不好理解, 如何从prefix得到对应的定位strategy?
其实在整个ElementFinder类__init__()的时候, 就初始化了这个self._strategies对象:
1 def __init__(self): 2 strategies = { 3 'identifier': self._find_by_identifier, 4 'id': self._find_by_id, 5 'name': self._find_by_name, 6 'xpath': self._find_by_xpath, 7 'dom': self._find_by_dom, 8 'link': self._find_by_link_text, 9 'partial link': self._find_by_partial_link_text, 10 'css': self._find_by_css_selector, 11 'jquery': self._find_by_sizzle_selector, 12 'sizzle': self._find_by_sizzle_selector, 13 'tag': self._find_by_tag_name, 14 'scLocator': self._find_by_sc_locator, 15 'default': self._find_by_default 16 } 17 self._strategies = NormalizedDict(initial=strategies, caseless=True, spaceless=True) 18 self._default_strategies = strategies.keys()
看到"定位元素":"定位策略"的一串列表, 恍然大悟. 不管这个在robot.utils中的自定义字典类NormalizedDict的具体实现, 该类型的对象self._strategies基本用途就是: 所有元素定位策略的方法列表通过key值查询. 这样一来就好理解了, find()方法最终也只是起到了派发的作用, 给strategy对象赋予了属于它的定位方法.--->再往下接着跟self._find_*方法心好累, 下次吧......
接下来就可以看find(self, browser, locator, tag=None)源码了, 理解了strategy就没什么特别的地方了:
1 def find(self, browser, locator, tag=None): 2 assert browser is not None 3 assert locator is not None and len(locator) > 0 4 (prefix, criteria) = self._parse_locator(locator) # 从locator对象中提取prefix和criteria 5 prefix = 'default' if prefix is None else prefix 6 strategy = self._strategies.get(prefix) # self._strategies对象的属性石robot.util中的NormalizeDict(标准化字典) 7 if strategy is None: 8 raise ValueError("Element locator with prefix '" + prefix + "' is not supported") 9 (tag, constraints) = self._get_tag_and_constraints(tag) 10 return strategy(browser, criteria, tag, constraints) # 返回strategy的find结果
VI. Module: resources
Selenium只提供了和firefox相关的资源文件, 受到 “Selenium webdriver 学习总结-元素定位” 文章的启发, 个人觉着应该是firefox提供了丰富全面的组件, 能够集成selenium的缘故吧. 不是做Web开发不了解, 但firefox的功能强大丰富是一直有所耳闻. 所以我们不妨推测, resources的内容就是对firefox的组件和方法的描述