开源应用架构之​SeleniumWebDriver

来源:互联网 发布:小精灵字幕软件 编辑:程序博客网 时间:2024/06/06 00:26

前不久,InfoQ向大家推荐了几本有关软件架构的新书,引起了国内读者的广泛兴趣。​其中一本是《开源应用架构(The Architecture of Open Source Applications)》,来自知名开源项目的各位作者对软件的设计进行了说明。通过对这些成功的系统架构进行概览,让软件工程师可以彻底了解最佳实践和陷阱。InfoQ中文站响应读者的需求,整理了该书有关知名开源软件架构的精彩内容,供国内开发社区借鉴。本期介绍的是著名浏览器自动化工具Selenium WebDriver的软件架构,第一部分主要分享了Selenium WebDriver的演变历史和架构观点。

Selenium是一个浏览器自动化工具,通常用来编写​Web应用的端到端测试。浏览器自动化工具​准确执行你所期望的行为:自动化浏览器的某个控件,从而可以自动重复执行任务。这听起来像是一个很容易解决的问题,但是正如我们即将看到的那样,其实Selenium成功的背后凝聚了大量的工作。

介绍Selenium WebDriver软件架构的技术专家是​来自Google的Simon Stewart,他是Selenium的核心贡献者和Selenium WebDriver的创建者。

Simon Stewart首先谈起了Selenium的组成部分:

在介绍Selenium架构之前,最好先了解一下该项目的各个相关组成部分是如何结合在一起的。从较高的层次看,Selenium由三种工具组成。第一个工具Selenium IDE,是Firefox的扩展插件,支持用户录制和回访测试。录制/回访模式​存在局限性,对许多用户来说并不适合,因此第二个工具——Selenium WebDriver提供了各种语言环境的API来支持更多控制权和编写符合标准软件开发实践的应用程序。最后一个工具——Selenium Grid帮助工程师使用Selenium API控制分布在一系列机器上的浏览器实例,支持并发运行更多测试。在项目内部,它们分别被称为“IDE”、“WebDriver”和“Grid”。 

​追根溯源,Selenium和WebDriver最初是两个独立的项目,Simon Stewart​解释了发展的历史:

Jason Huggins在2004年发起了Selenium项目,当时他在ThoughtWorks公司开发内部的时间和费用(Time and Expenses)系统,该应用使用了大量的JavaScript。虽然Internet Explorer在当时是主流浏览器,但是ThoughtWorks还使用一些其他浏览器(特别是Mozilla系列),当员工在自己的浏览器中无法正常运行T&E系统时就会提交bug报告。当时的开源测试工具要么关注单一浏览器(通常是IE),要么是模拟浏览器(如HttpUnit)。购买商业工具授权的成本会耗尽这个小型内部项目的有限预算,所以它们都不是可行的测试选项。​

在自动化困难的情况下,通常会依靠手动测试。​当开发团队规模很小或者构建发布非常频繁时,这种方式不太适用。同时,让人手动执行原本可以自动化的脚本也是一种对人力的浪费。沉闷重复的任务越无聊,人们工作就会越慢而且比机器犯更多错误。手动测试也不是一种选择。​  

幸运的是,所有被测试的浏览器都支持Javascript。Jason和他所在的团队有理由采用Javascript编写一种测试工具来验证应用的行为。他们受到FIT​(Framework for Integrated Test)的启发,使用基于表格的语法替代了原始的Javascript​,这种做法支持那些编程经验有限的人在HTML文件中使用关键字驱动的方式来编写测试。该工具,最初称为“Selenium”,后来称为“Selenium Core”,在2004年基于Apache 2授权发布。​

Selenium的表格格式类似于FIT的​ActionFixture。表格的每一行分为三列。第一列给出了要执行的命令名称,第二列通常包含元素标记符,第三列包含一个可选值。例如,如下格式表示了如何在名称为“q”的元素中输入字符串“Selenium WebDriver”:

type   name=q   Selenium WebDriver

因为Selenium过去使用纯JavaScript编写,它的最初设计要求开发人员把准备测试的应用和Selenium Core、测试脚本部署到同一台服务器上以避免触犯浏览器的安全规则和JavaScript沙箱​策略。在实际开发中,这种要求并不总是可行。更糟的是,虽然开发人员的IDE能够帮助他们快速处理代码和浏览庞大的代码库,但是没有针对HTML的相关工具。​人们很快意识到维护一个中等规模的测试集是笨拙而痛苦的过程。​

为了解决这个问题和其他问题,我们编写了HTTP代理,这样所有的HTTP请求都会被Selenium截获。使用代理可以绕过“同源”规则(浏览器不支持Javascript调用任何当前页面所在服务器以外的其他任何东西)的许多限制​,从而缓解了首要弱点。这种设计使得采用多种语言编写Selenium绑定成为可能:它们只需把HTTP请求发送到特定URL。连接方法基于Selenium Core的表格语法​​严格建模,称之为“Selenese”​。因为语言绑定在远程控制浏览器,所以该工具称为“Selenium Remote Control”或者“Selenium RC”。

就在Selenium处于开发阶段的同时,另一款浏览器自动化框架WebDriver也正在ThoughtWorks公司的酝酿之中。WebDriver的最初代码在2007年初发布。WebDriver项目的初衷是把端对端测试与底层测试工具隔离开。通常情况下,这种隔离手段通过适配器(Adapter)模式完成。WebDriver正是来源于该方法在许多项目上的不断实践应用,最初是HtmlUnit的封装,工具发布后很快开始支持Internet Explorer和Firefox。

在WebDriver最初发布时,与Selenium RC存在显著差异,尽管它们都属于浏览器自动化的API工具。对于用户来说,最明显的区别在于Selenium RC提供基于字典的API,所有方法都在一个类中开放,而WebDriver的API更面向对象。此外,WebDriver仅支持Java,而Selenium RC提供广泛的语言支持。技术差异也很明显:Selenium Core(RC的基础)基本上是JavaScript应用,运行在浏览器的安全沙箱之内。WebDriver则尝试原生绑定到浏览器中,绕开了浏览器的安全模型,代价就是框架自身的开发投入显著增加。​

在2009年8月,两个项目宣布合并,Selenium WebDriver就是合并的成果。目前,WebDriver支持的语言绑定包括Java、C#、Python和Ruby。它支持Chrome、Firefox、Opera和Android、iPhone浏览器。​此外,还有其他关联项目,不在同一源代码库中维护,但是和主项目(Selenium WebDriver)紧密合作,例如提供Perl绑定支持、BlackBerry浏览器支持,以及“无头”WebKit——用于持续集成的测试其无法正常显示的情况。最初的Selenium RC机制仍然维持,帮助WebDriver在浏览器不受支持的情况下提供支持。​

Simon Stewart在介绍Selenium WebDriver软件架构之前,谈到了架构和项目开发的重要主题。​概括如下:

  • 保持成本低廉。
  • 模拟用户。
  • 证明dirver运行良好......
  • ......但是你无需了解一切细节
  • 降低巴士因素(bus factor)。
  • ​偏爱JavaScript实现。
  • 所有方法调用都是RPC调用。
  • 我们是开源项目。

具体来说:

保持成本低廉​

在Y平台上支持X浏览器本质上是一种昂贵的​提议,无论是从开发还是维护角度考虑。如果我们能够找到办法维持产品的高品质而又不违背太多其他原则,那么就值得采纳。这种思想最明显的体现在我们尽可能得使用JavaScript编程,你稍后会看到。​

模拟用户
​WebDriver的设计目的是为了准确模拟用户与Web应用交互的方式。模拟用户输入的常用办法是利用Javascript合并和触发一系列事件(如果真实用户执行相同交互操作,应用会处理同样的事件)。“合成事件”(synthesized events)方法在面对不同浏览器、有时相同浏览器的不同版本时存在不少困难,触发的事件和相关值略有不同。为了不让问题复杂化,大多数浏览器处于安全原因不允许用户通过这种方式与表单元素(如文件输入元素)交互。

WebDriver总是尽可能的使用在操作系统层面​触发事件的方法。​因为这些“原生事件”不是由浏览器生成,所以这种方法​避免了​合成事件导致的安全限制,同时,因为它们是特定于具体操作系统的,​一旦在某个平台上的浏览器运行良好,在另一种浏览器上重用代码相对容易些。困难的是,这种方法​必须满足两点才可行:WebDriver与浏览器紧密绑定,同时开发团队在无需浏览器窗口获得焦点的情况下发送原生事件(因为Selenium测试运行时间较长,最好能支持同时在机器上执行其他任务)。目前,原生事件可用于Linux、Windows平台,不包括Mac OS X。​

不管WebDriver如何模拟用户输入,我们都在努力尽可能地模仿用户行为。​RC刚好相反,它提供的API层次远低于用户操作。​

证明Driver运行良好​

想让事情十全十美(motherhood and apple pie)​可能过于理想化了,但是我相信编写无法运行的代码是没有意义的。证明driver(指的是WebDriver API的特定实现。例如,Firefox和Internet Explorer各有自己的driver实现)在Selenium项目中运行良好的办法是我们拥有一套广泛的自动化测试用例。这些通常是“集成测试”,需要编译代码并使用浏览器与Web服务器交互,但是我们尽可能地编写“单元测试”,它不像集成测试,无需完全重新编译即可运行。目前,大约有500个集成测试和250个单元测试,涵盖所有浏览器。​我们在修补缺陷和编写新代码时会增加测试,我们的重点正转移到编写更多的​单元测试。​

并非所有​测试都要运行于每一个浏览器上。其中一些测试用于某些浏览器不支持的特定功能,或者在不同浏览器上处理方式不同的功能。例如,某些测试用于新的HTML5功能,这些功能并非所有浏览器都支持。尽管如此,每一个主流的浏览器都进行了充分的测试。可以想象,在不同平台上针对每种浏览器运行超过500个测试是一种极大的挑战,我们一直在努力面对。​

你无需了解一切细节​​

很少有开发人员精通各种语言和技术。因此,我们的架构需要帮助开发人员把他们的才华用于最擅长的地方,而无需处理不适合他们的代码片段。​

降低巴士因素​

在软件开发领域存在一种(非正式的)​概念,称为“巴士因素”。​它指的是关键开发人员的数量​,如果这些人遇到不幸——被大巴撞伤而离开项目,那么项目就无法继续进行。​像浏览器自动化这样复杂的技术特别能够证明巴士因素的重要性,因此我们的许多架构决策都希望能够尽可能提高关键开发人员的数量。​

偏爱Javascript实现

WebDiver在没有其他方式控制浏览器的情况下会使用纯Javascript驱动浏览器。这意味着我们添加的所有API都应该倾向于偏爱Javascript实现。举一个具体的例子,HTML5引入了LocalStorage,这是在客户端存储结构化数据的API。它通常在浏览器中使用SQLite实现。比较自然的实现方式是使用类似JDBC的技术为底层的数据存储提供数据库连接。最终,我们决定采用底层Javascript实现的API,因为通常数据库访问API与Javascript实现不太兼容。

所有方法调用都是RPC调用​

WebDriver控制运行在其它进程中的浏览器。一个很容易忽视的事实是,这意味着所有API调用都是RPC调用,因此框架的性能在于网络延迟上。在正常操作中,​这未必明显——大多数操作系统优化了到本机(localhost)的路由——但是随着浏览器和测试代码之间的网络延迟增加,对于API设计者和使用者来说,原本高效的调用会恶化。​

​这种情况给API的设计带来了一定压力。功能粗糙的较大规模的API可能会通过合并多个调用帮助减低延迟,但是这需要掌握平衡,时刻保持API的可读性和易用性。例如,有时候需要检测某个元素是不是对最终用户可见。我们不仅需要考虑各种CSS属性(可能需要通过查看父元素来推断),也应该检查元素的尺寸。最低限度情况下,API应该分别执行​所有这些检测。WebDriver把这些功能都合并到一个方法isDisplayed中。​

这是开源项目​

虽然严格意义上说,这不是一种架构观点,但还是要强调Selenium是一个开源项目。上面提到的所有观点联系在一起,表达的意思是:​我们希望尽可能的帮助新的开发人员易于参与项目。降低参与门槛的措施包括尽可能使所需知识浅显、使用的语言种类较少、依赖自动化测试验证。

最初该项目被划分成一系列模块,每一个模块​代表了一种特定的浏览器,其他的模块是通用代码和支持代码。每一个绑定的代码树保存在这些模块下面。这种方法对于类似Java和C#​的语言来说非常有用,但是对于Ruby和Python的开发人员来说很痛苦。这种情况直接导致了有限的参与者,只有一小部分人参与Python和Ruby的绑定工作。为了解决这个问题,在2010年的十月和十一月,项目源代码被重新组织,Ruby和Python代码存放在每种语言的独立顶级文件夹中。这种方式符合开源开发人员的期望,立刻吸引了社区的广泛参与。​ 


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


开源应用架构之​SeleniumWebDriver(中)

作者 崔康 发布于2011年7月29日

Selenium团队最近发布了Selenium 2(又名SeleniumWebDriver)​。主要新功能是集成了WebDriver​——曾经是Selenium1(又名Selenium RC)的竞争对手。SeleniumRC在浏览器中运行JavaScript应用,而WebDriver通过原生浏览器支持或者浏览器扩展直接控制浏览器:

WebDriver针对各个浏览器而开发,取代了嵌入到被测Web应用中的JavaScript。与浏览器的紧密集成支持创建更高级的测试,避免了JavaScript安全模型导致的限制。除了来自浏览器厂商的支持,WebDriver还利用操作系统级的调用模拟用户输入。WebDriver支持Firefox (FirefoxDriver)、IE (InternetExplorerDriver)、Opera (OperaDriver) 和Chrome (ChromeDriver)。对Safari的支持由于技术限制在本版本中未包含,但是可以使用SeleneseCommandExecutor模拟。它还支持Android (AndroidDriver)和iPhone (IPhoneDriver) 的移动应用测试。它还包括一个基于HtmlUnit的无界面实现,称为HtmlUnitDriver。WebDriver API可以通过Python、Ruby、Java和C#访问,支持开发人员使用他们偏爱的编程语言来创建测试。

WebDriver的创建者​SimonStewart早在2009年八月的一份邮件中解释了项目合并的原因。

为何把两个项目合并?部分原因是WebDriver解决了Selenium存在的缺点(比如,能够绕过JS沙箱。我们有出色的API),部分原因是 Selenium解决了WebDriver存在的问题(例如支持广泛的浏览器),部分原因是因为Selenium的主要贡献者和我都觉得合并项目是为用户 提供最优秀框架的最佳途径。

在两个项目合并中出现了哪些架构方面的问题?学到了哪些经验和教训?​Simon Stewart在《开源应用架构》中做了详细的描述,本文是Selenium WebDriver架构系列文章的第二篇,对复杂性设计的优劣做了深入实际的分析。

处理复杂性

软件是模块构造起来的。这些模块很复杂​,作为API的设计人员,我们可以选择如何处理这种复杂性。极端情况下,​我们可能会传播这种复杂性,这意味着​API的每一位用户都需要牵涉其中。另一个​极端情况是承担尽可能多的复杂性并将其隔离在某个地方。这个地方对于许多想一探究竟的API用户来说​黑暗而恐怖。折中方案则是API的用户,如果无须深入了解实现​细节,那么只需面对当前所遇到的复杂性​即可。​

WebDriver的开发人员更倾向于发现并在少数地方隔离这些复杂性,而不是传播它。这么做的原因之一是为了用户。看看我们的bug列表就​会知道,​他们特别善于发现问题和缺陷,但是因为许多用户不是开发人员,复杂的API不会受欢迎。我们试图让API正确地引导大家。例如,​考虑下面来自早期SeleniumAPI的方法,每一个都用于设置输入元素的值:​

  • type
  • typeKeys
  • typeKeysNative
  • keydown
  • keypress
  • keyup
  • keydownNative
  • keypressNative
  • keyupNative
  • attachFile

下面是WebDriverAPI中的等价方法:​

  • sendKeys

如前所述,这凸显了RC和WebDriver之间的主要思想差异——WebDriver在努力模拟用户,而RC在较低层次提供的API​让用户难以或者无法使用。typeKeys和typeKeysNative之间的​区别在于前者总是使用合成事件(synthetic event)​,而后者则尝试利用AWTRobot输入键值。​令人失望的是,AWTRobot发送按键事件给具有焦点的任意窗口,也就是说可能不是浏览器。相比之下,WebDriver的原生事件,直接把事件发送给窗口处理函数,避免了浏览器窗口必须具有焦点的要求。​

WebDriver设计​

团队将WebDriver的API定位为“基于对象的”。接口被明确定义并努力坚持只包含一个角色或者责任,而不是将每一个可能的HTML标记模块化为单独的类,我们只有一个WebElement接口。​通过这种方式,开发人员使用支持自动补全的IDE即可被提示下一步工作。其结果类似于下面的代码片段(Java语言):

WebDriver driver = newFirefoxDriver();
driver.<user hits space>

此时,包含13个方法的短列表显示出来,用户选择其中一个: 

driver.findElement(<userhits space>)

大多数IDE现在显示预期参数的类型提示,在这个例子中是“By”。By包含许多预定义的静态工厂方法。我们的用户可以快速地完成刚才的代码:

driver.findElement(By.id("some_id"));

基于角色的接口 

考虑一下简化的Shop类。每天,它需要进货,并与Stockist合作发布新的货单。每月,它需要付工资和税。为了描述清楚,我们假设它通过使用Accountant完成这些事情。一种建模结果如下所示: 

public interface Shop {
     void addStock(StockItem item, int quantity); 
     Money getSalesTotal(Date startDate, Date endDate); 
}

我们有两种选择来定义Shop、Accountant和Stockist之间的接口的边界。图1显示了一种理论上的选择。

这意味着Accountant和Stockist将把Shop作为各自方法的参数。缺点是,Accountant不可能真的想要处理货架,而让Stockist了解Shop添加的大量价签也不合适。因此,更好的一种思路如图2所示。

我们将需要两个Shop必须实现的接口,但是这些接口清晰的定义了Shop为Accountant和Stockist承担的角色。它们都是介于角色的接口:

public interfaceHasBalance { 
     Money getSalesTotal(Date startDate, Date endDate); 


public interface Stockable { 
    void addStock(StockItem item, int quantity); 


public interface Shop extends HasBalance, Stockable {
}

我发现UnsupportedOperationExceptions等让人非常不适,但是我们需要某些东西以支持对部分用户暴漏部分功能而不会影响API的其他部分。为此,WebDriver广泛使用了基于角色的接口。例如,有一个JavascriptExecutor接口提供了在当前页面环境中执行任意Javascript语句块的功能。WebDriver实例对该接口的成功映射可以提示你利用该方法完成自己的工作。

图1:Accountant和Stockist依赖Shop

图2:Shop实现了HasBalance和Stockable

处理组合爆炸​

考虑到WebDriver​支持广泛的浏览器和语言,我们首先想到稍有不慎,就会面临维护成本的大量攀升。​假设X种浏览器和Y种语言​,我们很容易就会掉进X×Y种实现中。

减少WebDriver支持的编程语言种类是降低成本的途径之一,但是我们基于两种原因不想这样做。首先,从一种语言切换到另一种时人们会承受认知负荷,因此对用户来说如果测试框架(WebDriver)能够允许他们采用在日常开发中使用的编程语言来编写测试,那么这是巨大的优势。其次,在单一项目中混合多种语言可能会让团队感觉不舒服,而且公司的编码规范和需求通常要求技术单一纯正性(虽然我们愉快的看到,第二点理由随着时间推移越来越淡化),因此,减少支持语言的种类不是可选项。

减少支持浏览器的数量也不是一种选择——想想看,当我们决定在WebDriver中淘汰对Firefox2的支持时,就遇到了强烈的抗议,而事实上当我们作出这个决定时,Firefox2只占了浏览器市场份额不到1%。

我们唯一的选择是努力使所有浏览器对语言绑定的外观相同:它们应该提供统一的接口,可以轻松地通过各种语言解决。更重要的是,我们希望语言绑定本身尽可能的易于编写,这意味着需要尽可能的使它们保持简洁。我们在底层driver中放入了尽可能多的逻辑来支持这种设计:我们无法放入dirver的每一块功能都意味着需要通过我们支持的每一种语言实现,而这代表了一大块工作量。

这里举一个例子,IEdriver成功地把定位和启动IE的功能放入了主要驱动逻辑中。虽然这会导致在dirver中编写惊人数量的代码,但是用于创建新实例的语言绑定只需对driver的单一方法调用。相比之下,Firefox无法做这种改动。在Java语言中,这意味着我们有三个主要的类来处理配置和启动Firefox,大约1300行代码。这些类在每一种支持FirefoxDriver的语言绑定中都是重复的,无须依赖Java服务器。这会有大量的多余代码需要维护。

WebDriver设计中的缺陷

通过这种方式发布功能的缺陷在于除非有人知道某个特定的接口存在,否则他们可能不会意识到WebDriver支持这种功能,在API的可发掘性上存在缺憾。当然在WebDriver刚发布的时候,我们会投入大量时间来指导人们找到合适的接口。现在我们已经花费大量精力来编写文档,随着API获得广泛应用,用户会越来越容易的找到所需的文档。

我认为API有一个地方设计的非常差。我们有一个接口称为RenderedWebElement,其中包含一些奇怪的方法来查询元素的渲染状态(isDisplayed、getSize和getLocation),执行操作(hover和拖拽方法),而且还提供方法获取特定CSS属性的值。创建它的原因是HtmlUnit驱动没有提供所需的信息,但是Firefox和IE驱动提供了。它最初只有一部分方法,后来我经过苦苦思索又增加了其他方法。这个接口目前众所周知,艰难的选择在于是否保持API的丑陋之处,或者删除它。我更倾向于不要遭遇“破窗”理论,因此,在Selenium2.0发布之前修补它非常重要。结果就是,在你读到这些文字时,RenderedWebElement可能已经消失了。

从实现者的观点来看,紧密绑定浏览器也是一种设计缺陷,虽然无法避免。支持新浏览器时需要投入巨大的努力,经常需要数次尝试才能找到正确方法。具体的例子就是,Chrome驱动经过了四次完全重写,IE驱动也有三种关键重写。紧密绑定浏览器的优点在于它提供了更多控制权。

布局和Javascript

浏览器自动化工具基本上由三部分构成:

  • 与DOM交互的方法
  • 执行Javascript的机制
  • 一些模拟用户输入的办法

本节重点介绍第一部分:提供与DOM交互的机制。浏览器的办法是通过Javascript,所以看起来与DOM交互的理想语言也是它。虽然这种选择似乎显而易见,但是在考虑Javascript时需要平衡一些有趣的挑战和需求。

像多数大型项目一样,Selenium使用了分层的库结构。底层是Google的Closure库,提供原语和模块化机制来协助源文件保持精简。在此之上,有一个实用工具库,提供的函数包括简单的任务,如获取某个属性值、判断某个元素是否对用户可见,还包括更加复杂的操作,如通过合成事件模拟用户点击。在项目中,这些被视为提供最小单元的浏览器自动化,因此称之为浏览器自动化原子(BrowserAutomation Atom)。最后,还有适配层来组合这些原子单元以满足WebDriver和Core的API协议。

图3:Selenium Javascript库的层次结构

选择Closure库基于几种原因。主要理由是Closure编译器理解库使用的模块化技术。Closure编译器的目标是输出Javascript。“编译”可以简单到按照依赖顺序查找输入文件、串联并漂亮的打印出来,也可能复杂到进行精细的改动和删除死代码。另一种不可否认的优势是团队中采用Javascript编程的几位成员对Closure库非常熟悉。

当需要与DOM交互时,“原子”库的代码会被用于项目中的各个角落。对于RC和那些大部分由Javascript编写而成的driver来说,这些库被直接使用,通常编译为单个巨大的脚本。对于采用Java编写的driver,来自WebDriver适配层的各个函数在编译的时候会启用完整优化,生成的Javascript在JAR中作为资源包含进来。对于采用C语言编写的driver,如iPhoneIE驱动,不仅各个函数被通过完整优化来编译,而且生成的输出文件被转换成定义在头文件中的常量,通过driver的正常Javascript执行机制来执行。虽然这看起来有些奇怪,但是这种做法使Javascript放在底层驱动中,无须在各处暴露原始的代码。

因为原子库应用广泛,所以在不同浏览器之间确保一致的行为是可行的,因为库采用Javascript编写,而且无需提升权限来执行开发周期,所以方便、快捷。Closure库可以动态加载依赖,因此Selenium开发人员只需编写测试并在浏览器中加载,修改代码并在需要时点击刷新按钮。一旦测试在浏览器中通过,很容易在另一个浏览器中加载并确保通过。因为Closure库在抽象屏蔽浏览器之间的差异方面做得很好,这就足够在持续构建中在每一种支持的浏览器中运行测试集以衡量是否通过。

最初Core和WebDriver存在许多相同的代码——通过略微不同的方式执行相同的功能。当我们开始关注原子库时,这些代码被重新梳理,我们努力找出最合适的功能。毕竟,两个项目都被广泛应用,它们的代码非常健壮,因此把一切都丢掉从零开始不仅浪费而且愚蠢。通过对每个原子库的分析,我们找出了可以使用的部分。例如,Firefoxdriver的getAttribute方法从大约50行缩减到几行,包括空白行在内:

FirefoxDriver.prototype.getElementAttribute= 
function(respond, parameters) { 
    var element = Utils.getElementAt(parameters.id,respond.session.getDocument()); 
    var attributeName = parameters.name; 
    respond.value = webdriver.element.getAttribute(element,attributeName); 
    respond.send(); 
};

倒数第二行中,respond.value的赋值调用了原子级的WebDriver库。

原子库是本项目若干架构思想的实际演示。当然,它们满足了API的实现应该倾向于Javascript的需求。更出色的是,用一个库在代码库中分享,以前一个缺陷需要在多种实现中验证和修复,现在只需在一个地方修改即可,这种做法降低了变化的成本,同时提高了稳定性和有效性。原子库也使项目的“巴士”因素更优化。因为通常的Javascript单元测试可以用于验证缺陷是否修复,所以参与到开源项目中的障碍要比之前需要了解每一个driver如何实现的时候更低。

使用原子库还有另外一个好处。模拟现有RC实现但由WebDriver支持的分层对团队尝试以可控的方式迁移到更新的WebDriver API是一种重要的工具。因为Selenium Core是原子化的,所以单独编译每一个函数是可行的,使得编写这种模拟层易于实现而且更准确。

当然,这种做法也存在缺点。最重要的是,把Javascript编译成C常量是一种非常奇怪的事情,它总是阻碍那些想参与C语言编程的项目贡献者。而且很少有开发人员能够了解所有浏览器并致力于每一种浏览器上运行所有测试——很可能有人会不小心在某处引入回归问题,我们需要花时间找到问题,如果持续构建很多的话则更需精力。

因为原子库规范了浏览器之间的返回值,所以可能存在意想不到的返回值。例如,考虑如下HTML:

<inputname="example" checked>

checked属性值依赖于使用的浏览器。原子库规范了该值和HTML 5标准中定义的其他Boolean属性为“true”或者“false”。当该原子量被引入代码库后,我们发现有许多地方大家都做了依赖于浏览器的假设(觉得返回值应该是什么样的)。虽然这些值现在都一致了,但是我们花了很长时间来向社区解释发生了哪些变化以及这样做的原因。

 

原创粉丝点击