Selenium WebDriver中文翻译

原文来自《开源软件架构》(The Architecture of Open Source Applications)书中的 “ http://aosabook.org/en/selenium.html |Selenium WebDriver ”章节。

Selenium是一个浏览器自动化工具,通常用于编写端到端的web应用测试脚本。正如字面意思,浏览器自动化工具能够自动化对浏览器的控制,因此可用于自动化重复的测试任务。这看上去好像是个能够轻易解决的问题,但是在后面我们将会发现,实现这个功能的背后,做了很多工作。

在描述Selenium的架构之前,先理解各种相关的项目是如何组合在一起的是有很有帮助的。从较高的层面上来看,Selenium是一套整合了三个工具的工具集。其一,Selenium IDE,是Firefox的一个扩展,支持用户记录和回放测试脚本。但是记录/回放模式较为局限,对很多用户来说并不适用。所以需要第二个工具,Selenium WebDriver,它提供了多种语言的API,可用于进行更多的操纵和构建标准软件开发实践的应用。其三,Selenium Grid,它支持Selenium API控制分布于不同机器上的浏览器实体,并能够并行的运行测试任务。在这个项目中,三个工具简称“IDE”、“WebDriver”、“Grid”。本章将探讨Selenium WebDriver的架构。

本章撰写于2010年末,Selenium2.0版本外部测试时期。如果你是在这以后阅读这本书,Selenium的架构会有所演进,你会看到本章所描述的架构的 选择是如何被展开。如果你是在这之前阅读这本书,恭喜你!你成功get了一个时光机。你能给我些彩票的中奖号码吗?

16.1. 发展历程

Jason Huggins在2004年开启了Selenium项目,当时他正在ThoughtWorks公司为一个内部的Time and Expenses (T&E)系统工作,这个系统的编写大量的运用了Jacascript。虽然IE浏览器在当时是主流浏览器,ThoughtWorks还是用了多种可选择的浏览器(特别是Mozilla系列),并且当T&E软件不能在他们选择的浏览器上工作时会生成Bug报告。那个时期的开源测试工具,要么只能处理单个浏览器(特别是IE),要么只能处理模拟浏览器(比如HttpUnit)。一个商用工具许可证的花费差不多能花光一个内部小规模项目的有限预算,所以当时没有多少可用的测试工具可供选择。

由于自动化的困境,测试工作通常都依赖于手动测试。当一个开发队伍非常小或者软件发布非常的频繁时,这种方法不具有可扩展性。另外,要求人们逐句执行一个本可以自动化运行的脚本是对人力资源的一种浪费。更直白的说,对于一个反复的枯燥的任务,人为处理较机器处理来说效率更低且错误更多。手工测试测试不是一个好的选择。

幸运的是,所有有待进行测试的浏览器都支持Javascript。这解释了为什么Jason和他所在的小组选择了用Javascript来编写一个测试工具,鉴于Javascript是一个能够用于证明应用行为的语言。工具的完成是受到FIT的启发,FIT是一个基于表格的语法被架构在原始的Javascript之上,这种语法能够被编程经验不多的人所使用,它是一种类似于HTML文档的关键字驱动的语法。这个工具一开始叫做“Selenium”,但是后来改称为“Selenium Core”,并且基于Apache 2协议发布于2004年。

Selenium的表格格式的结构类似于FIT中的Action Fixture。表格中每行分为三列。第一列给出了要执行的命令的名字,第二列通常包含一个元素标示符,第三列包含一个可选值。比如,以下就是如何将一个字符串“Selenium WebDriver”表示为以q命名的元素标示符:

name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
  
由于Selenium是纯粹用Javascript编写的,为了避免陷入与浏览器安全性策略、Javascript 沙盒(Sandbox)之间的冲突,它的最初设计要求开发者掌握Selenium Core和部署到同一台作为被测试应用的服务器上的测试脚本,这件事对开发人员来说不一定实际且合理。更糟糕的是,即使一个开发者的集成开发环境给他们提供了能够很快操纵代码和浏览一个大型的代码库的能力,对于HTML却没有相关的工具。以上清晰地表明了,维护一个就算只是中型的测试套件都是一件困难又痛苦的事情。

要解决上述困难还有其他一些问题,一个HTTP代理模式出世,这个代理模式使得HTTP请求都能被Selenium截获。使用这个代理模式能够回避“同主机源”策略的诸多限制,例如一个浏览器不允许Javascript去调用除了当前页面所在的服务器以外的任何东西,而此代理模式环节了这个策略的首要弱点。这种设计为编写Selenium这个与多种语言捆绑的工具开辟了新方法:它们其实只需要能够发送HTTP请求到一个特定的URL。这个有线格式很多的效仿了Selenium Core基于表格的语法,并且随着基于表格的语法演变成了被称作“Selenese”的语言。由于这种语言绑定是在远端控制浏览器,所以该工具也被称为“Selenium Remote Control”(Selenium远程控制)或“Selenium RC”。

当在开发Selenium时,ThoughtWorks公司里正酝酿着另一个浏览器自动化框架——WebDriver。WebDriver的初代源代码发布于2007年初。WebDriver是从一个希望从底层测试工具中分离出端到端测试的项目工程中衍生出来的。通常,这种分离手段是通过适配器(Adapter)模式完成的。WebDriver产生于在多个项目中不断运用这种方法的实践应用,在最初只是一个对HtmlUnit的封装。工具发布后迅速的支持了IE和Firefox浏览器。

在WebDriver最初发布时,它和Selenium RC之间存在显著的差异,虽然它们都属于浏览器自动化API的软件领域内的应用。对于用户来说,这两者的显著区别在于Selenium RC提供基于字典的API,它的所有方法在一个类中都是透明的,然而WebDriver的API更加面向对象化。另外,WebDriver只支持Java语言,然而Selenium RC提供共犯的可供选择的语言。还有一个重要的技术上的差异:Selenium Core(RC的基础)在本质上是一个Javascript应用,运行在浏览器安全沙盒内部。WebDriver则试着去把自己绑定到浏览器绑中,以回避浏览器的安全模式,代价是显著增加了框架本身开发工作的成本。

在2009年8月,两个项目宣布合并,Selenium WebDriver就是两者合并后的项目。在我写这篇文章时,Selenium WebDriver已经支持了包括Java、C#、Python、Ruby的语言绑定,提供了Chrome、Firefox、IE、Opera还有Android和iPhone上的各种浏览器的支持。它的关联项目,不保存在同一个源代码仓库中,但是与主项目密切合作,例如提供了Perl的语言绑定,在BlackBerry浏览器上的实现,和用来在持续集成的服务器上运行无法正常显示的测试集的“无头”WebKit。原来的Selenium RC所使用的机理仍然保持者,帮助在原来WebDriver不支持的浏览器上提供支持。


## 16.2. 关于专业术语的题外话 ##

不幸的是,Selenium项目使用了很多的术语。回顾一下我们已经提到过的术语:

* Selenium Core是原Selenium实现的核心部分,是一个用于控制浏览器的Javascript的脚本集。常简称为“Selenium”或是“Core”。

* Selenium RC 是Selenium Core的语言绑定的名字,一般性的却又令人疑惑的简称为“Selenium”或是“RC”。这部分现在已经被Selenium WebDriver所替代,通过将RC的API改名为“Selenium 1.x API”。

* Selenium WebDriver与RC的功能相似,且包含了原1.x的绑定件。1.x绑定件指的是语言绑定和个别浏览器控制代码的实现。一般把这部分简称为“WebDriver”,有时也称作Selenium 2. Doubtless,以后将简称为“Selenium”。

精明的读者会注意到“Selenium”这个称号所指广泛。幸运的是,文章一般都会清楚的表明所指的是哪个Selenium。

最后,还有个常用词,我只能用直白的介绍:“driver”是WebDriver的API中的一个特定的实现的名字。例如,Firefox driver,IE driver。


## 16.3. 架构主题 ##

在我们开始理解这些独立的模块是怎么结合在一起之前,我们先要弄明白此项目的架构和开发的重要主题。简明扼要的来说:

* 保持低成本
* 模拟用户
* 证明driver运行良好…
* …但你不需要理解一切细节
* 降低巴士因素(bus factor)
* 理解Javascript的实现
* 所有的方法调用都属于RPC调用
* 我们是一个开源项目


### 16.3.1. 保持低成本 ###

在Y平台上支持X浏览器,不管在最初的开发还是维护的角度上去考虑,本就是一个昂贵的命题。如果我们能找到在不违反太多其他原则的前提下保持产品的高品质的方法,那么这就是我们所希望走的路线。这种路线最明显地体现在我们尽可能的使用的Javascript编程上,很快你就会了解到。


### 16.3.2. 模拟用户 ###

WebDriver是为了精确模拟用户和web应用交互的方式而设计的。模拟用户输入的一般方式是利用Javascript来综合和触发一系列的事件,在应用的角度上来看这一系列事件和真实用户的交互事件是相同的。这一系列的综合事件(synthesized events)方法困难重重,原因是每个浏览器(有时是同一个浏览器的不同版本)之间相关数值稍有不同就会得到不同的事件集。对于一个复杂的问题,大多数的浏览器出于安全考虑不会允许用户用表单元素(比如文件输入)的方式进行交互。

WebDriver尽可能的选择在操作系统的层面上使用触发事件的方式。由于这些“原声事件”(native events)不会由浏览器所产生,这个方法回避了在合成事件上的安全问题,同时,因为他们是系统依赖的,一旦在某个特定的平台上的浏览器运行良好,在其他的浏览器上重用代码就相对容易了。悲哀的是,这个方法必须满足两点:WebDriver与浏览器密切绑定在一起,开发团队已找到怎么最好地在浏览器窗口是非选中状态下发送原生事件的方法(因为Selenium的测试脚本的运行需要一些时间,当脚本在运行的时候,设备最好能够执行别的任务)。目前,这意味着原生事件可以在Linux和Windows平台上使用,却不能在Mac OS X上使用。

不管WebDriver是如何模拟用户输入的,我们在尽可能接近的去模仿用户行为。这与RC相悖,它提供的API在层次上远低于用户操作。


### 16.3.3. 证明driver运行良好 ###

一个十全十美(motherhood and apple pie)的东西,听起来有点理想主义,但是我相信如果它不能运行的话,码代码就无意义了。我们证明driver能够在Selenium项目上运行的方法是,用一套广泛的自动化测试样例集对driver进行测试。通常选用“集成测试”作为样例,要求编译代码并运用与web服务器交互的浏览器,但是我们尽可能的编写“单元测试”作为样例,与集成测试不同的是,“单元测试”不需要全部重新编译就能够运行。目前,准备了大约500个集成样例和250个单元测试样例,覆盖所有浏览器。当我们修复问题和编写新的代码的时候,我们增加了更多的样例,而这些样例更加倾向于单元测试。

并非所有的测试脚本都能在每一个浏览器上运行。有些脚本是用来测试特殊的功能,这些功能在有些浏览器上可能并不支持,或是这些功能在不同的浏览器上的实现方法不同。例如,样例会包括一些对新的HTML5的特性的测试,然而并非所有的浏览器都支持HTML5。尽管如此,每个主流的桌面浏览器都拥有充分的能够在其上运行的测试样例子集。可以想象,找到一种可以在多个平台上对每个浏览器运行500多个测试脚本的方法是一个重大的挑战,这也是我们所持续奋斗的目标。


### 16.3.4. 你不需要理解一切细节 ###

很少有能够精通和熟悉我们使用的所有的语言和技术的开发者。因此,我们的架构需要帮助开发者专注于他们最擅长的才能,而不需要他们处理不熟悉的代码片段。


### 16.3.5. 降低巴士因素(bus factor) ###

在软件开发者之间有一个(非正式的)概念,叫做“巴士因素”。它是指因为遭遇一些可怕的事情——也许是被公车撞了——而离开了项目组,导致项目在一个阶段内无法继续进行的关键开发者的数目。像浏览器自动化这样复杂的项目巴士因素特别的重要,因此我们的很多架构上的选择是为了尽可能的提高巴士因素的数值。


### 16.3.6. 理解Javascript的实现 ###

WebDriver在没有别的方法控制浏览器的情况下变回使用纯Javascript去驱动浏览器。这意味着我们添加的所有API都要能够与Javascript的实现“相容”。一个具体的例子,HTML5引进了LocalStorage机制,这是在客户端存储结构化的数据的API。这个机制通常在使用了SQLite的浏览器上实现。比较自然的实现是提供能够与底层数据存储关联的数据库,通过类似于JDBC的API。最终,我们决定使用一个密切的模拟了底层Javascript实现的API,因为模拟典型的数据库访问的API与Javascript的实现不相兼容。


### 16.3.7. 所有的方法调用都属于RPC调用 ###

WebDriver控制运行在其他进程里的浏览器。尽管很容易被忽视,但是这意味着每一个它的API调用都是RPC调用,因此该框架的性能受限于网络延迟。在正常运行时,这可能不算特别明显——大多数的操作系统会优化到本地的路由——但是当浏览器和测试代码之间的网络延迟增加时,那些高效的调用会恶化,无论是对于API的设计者还是使用者来说。

这个设计介绍了API设计的一些张力考虑。一个较大规模的功能粗糙的API,能够通过多重函数调用降低延迟,但是这点会被保持API的性能和易用性所限制。比如,有一些必做的检查来确定一些元素对于一个终端用户是否是可见的。我们不仅需要考虑需要从父元素中推断出来的不同的CSS属性,还需要检查元素的尺寸。API在最低限度下都会逐个检查这些方面。WebDriver把这些检查都合并到单一的''isDisplayed''方法中。


### 16.3.8. 最后:这是一个开源软件 ###

虽然这一点严格说起来不算是一个架构设计要点,但是需要强调Selenium是一个开源项目。基于希望对一个新的开发者来说尽可能容易的上手的主题,才有了上述所有的特点。我们希望达到以下便于开发的特点:使知识的深度要求尽可能的浅;使用尽可能少的语言;通过自动化测试验证。

起初这个项目被分为一系列的模块,每个模块代表一个特定的浏览器,还有额外的模块用于通用代码和支持使用代码。每个绑定件的代码树存储在这些模块之下。这个方法对于很多语言来说是有意义的,如Java和C# ,但是对于Ruby和Python的开发人员来说是很痛苦的。这一点在相对代码贡献人数上显著的表现了出来,屈指可数的人有能力并且有兴趣去完成Python和Ruby的绑定件工作。为了解决此问题,在2010年的十月到十一月期间重构了源代码,Ruby和Python代码都存储在各自独立的顶级目录下。这更加契合了这些语言的开源开发者的期望,立即吸引了社区的广泛参与。


## 16.4. 面对复杂性 ##

软件是模块化的。模块是复杂的,作为一个API的设计者,我们需要决定将复杂性置于何处。在一个极端的情况下,我们希望均匀的平衡复杂性,这意味着每个API的使用者需要参与其中的设计。另一个极端的建议,吧复杂性尽可能的放在一个单独的部分。这个单独的部分对于不得不去实现它的人来说是黑暗的恐怖的,但作为交换的是,这个API的使用者虽然不需要了解其中的实现,但是需要为这个复杂性预付一些代价。

WebDriver的开发者更多学会的是找到一些放置复杂性的地方,而不是均衡复杂性。原因在于我们的使用者,他们特别擅长于在扫了一眼我们列出来的bug清单后,发现一些问题,但是因为他们大多数都不是开发者,所以一个复杂的API效果不会很好。我们力求能够提供一个能够引导用户正确使用的API。例如,考虑一下原始Selenium的API中包括的方法,每个方法都可以用于设置输入元素的值。

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

在WebDriver里有一个同等的API:

* sendKeys

就像之前讨论过的,这突出了RC和WebDriver之间主要的哲学上的差异:WebDriver努力去模拟用户,而RC提供的API则式在用户很难甚至不可能涉及到的层面上做处理的。typeKeys和typeKeysNative的不同之处在于:前者总是使用合成事件,而后者试图使用AWT的Robot类来分类键值。遗憾的是,AWT的Robot类只能发送键值给处于选中状态的窗口,而选中的窗口不需要一定是浏览器。相反地,WebDriver的原生事件是直接传送键值到窗口句柄,回避了必须要浏览器窗口处于选中状态的缺点。


### 16.4.1. WebDriver的设计 ###

项目组认为WebDriver的API是“面向对象的”。这些接口是明确定义的,并试图保持它们的角色和责任的唯一性,但是我们并没有对每一个可能的HTML标签都建模建立对应的类,我们值提供了单一的WebElement接口。通过沿用这种方法,使用支持自动补全的IDE的开发者们可以根据补全进行下一步。结果就是代码的会话会像这样(JAVA):

在这一点上,一个相关的包含13个方法的短列表会出现在界面上。用户选择一个:

driver.findElement()

1
2

现在大多数的IDE会显示一些关于方法所期望的参数类型的暗示,在这个例子中,期望一个“By”类型。为“By”对象预设的大量工厂方法被它自己声明成了静态方法。我们的用户很快就能写出这样的一行代码:

driver.findElement(By.id(“some_id”));

1
2
3
4


> 基于角色的接口
> 想象一个简化的''Shop''类。每天,它需要重新进货,并向跟它合作的''Stockist''提供新的货物。每个月,它需要支付工资和税。为了讨论的方便,我们想象一下,这里使用一个''Accountant''类。一种模拟的方法是这样的

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

1
2
>   对于划分Shop、Accountant、Stockist这三个定义之间的界限的时候,我们有两个选择。我们可以像图16.1中的那样,划出一条理论上的分界线。
> 这意味着''Accountant''类和''Stockist''类都要接受一个''Shop''类作为参数传递给他们各自的方法。缺点会计是不一定真的想要堆上架,并且让分销商意识到商店在价格上提高了很多不是一个好的实现。因此,一个更好的分界线是像图16.2那样的,我们需要两个由商店来实现的接口,但是这两个接口明确的定义了商店是一个需要满足会计的分销商的角色。以下是基于角色的接口:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

我发现了''UnsupportedOperationExceptions'' 的异常抛出,非常令人不愉快,但是需要允许有部分功能能够暴露给一些需要使用这些功能的用户,但是却不会把其余的API暴露给大多数用户。为此,WebDriver管饭使用基于角色的接口。例如,有一个''JavascriptExecutor''接口是用于提供在当前页面的上下文中执行Javascript任意块的能力。一个成功的WebDriver映射是一种能够期望它的方法能够有效的接口。

^![](/cdn/images/aosabook/67.png)
| [图16.1:基于Shop的Accountant和Stockist] |

^![](/cdn/images/aosabook/68.png)
| [图16.2:实现了HasBalance和Stockable接口的Shop] |


### 16.4.2. 处理组合爆炸 ###

首先,第一个想到的事情是,WebDriver对广泛浏览器和语言的支持显然会很快的遭遇维护成本不断攀升的问题,除非非常小心的处理。对于X个浏览器Y种语言,很容易陷入维护X*Y个实现的困境。

减少WebDriver所支持的语言是一个减少开销的办法,但是我们不想这样做,有两个原因。第一,当从一个语言转换成另一个语言需要承担认知负担,所以对使用这个框架的用户来说,能够用他们开发时常用语言来编写测试脚本是WebDriver的一大优点。第二,在打你的项目里杂糅多个语言是一个项目组非常不希望的看到的,公司的代码标准和需求一般规定技术的单一性(虽然,好消息是,我认为第二点随着时间的推移越来越不重要了),因此减少所支持的语言的数量不是一个可行的选择。

减少支持的浏览器数量也不是一个好的选项——当我们淘汰WebDriver中对FireFox2的支持时会发生严重的错误,除非它在浏览器市场中的占有率少于1%,否则我们不会做出淘汰的选择。

我们仅有可选择的选项是,试图使所有浏览器对于同一个语言绑定件来说都是一样的:他们应该提供一个统一的接口,用来容易的处理各种不同的语言。还有,我们想要语言绑定件自己能够尽可能的易于编词儿,这表明我们希望他们轻便。我们尽可能的向底层驱动器中放入各种逻辑,目的是:支持每个我们未能成功的放入驱动的功能模块是在所有我们支持的语言中实现,这意味着巨大的工作量。

例如,IE的驱动器成功的把定位和启动IE的责任放入主驱动逻辑中。虽然这导致了驱动器惊人的代码行数,用于创建新实例的语言绑定件归结到单一的方法调用到该驱动。相比之下,Firefox的驱动器未能成功实现这个改变。仅在Java的世界中,这意味着有三个大类用于控制Firefox的配置和启动,加起来大约有1300行代码。这些类在每个希望支持FirefoxDriver的语言绑定件中都是重复的,除非依赖于启动一个Java服务器。这需要大量的额外代码来维护。


### 16.4.3. WebDriver的设计缺陷 ###

决定以这种方式公开功能上的坏处在于,可能要等到有人发现了一个特殊的接口存在,他们才能意识到WebDriver是支持这种类型的功能的;在这样的API中有暴露的损失;当然,当WebDriver是新的,我们可能会花费大量时间仅仅是为了向人们指明一些特殊的接口。我们现在已经在文档上做了很多的努力,随着API被广泛的使用以后,用户就能更轻易地找到他们所需要的信息了。

有一个我认为我们的API设计的非常差的地方。我们有一个叫做''RenderedWebElement''的接口,它包含一个奇怪的方法杂烩,用于查询元素的渲染状态(''isDisplayed'', ''getSize'' and ''getLocation'')、在元素上执行操作(''hover'',拖放方法),还有一个便于得到特定的CSS属性值的方法。这个接口存在的原因是,HtmlUnit驱动不会暴露需要的信息,但是Firefox和IE的驱动会。一开始只有第一套方法,但是在我深刻的思考希望这个API该如何发展下去之前,我们就已经增加了其他的方法。这个接口现在已众所周知,所以要把这个API的丑陋的但是已经被广泛使用的一角保留下来,还是要尝试把它删除,是一个艰难的选择。

从一个实现者的角度来看,与浏览器绑定的太紧也是一个设计的缺陷,虽然这是不可逃避的设计。要支持一个新的浏览器显然需要付出努力,接着要让它正确运行需要付出更多。举一个具体的例子,chrome的驱动器经历了四次的整体重写,IE的驱动器也有三次的主体部分的重写。与浏览器绑定紧密的好处在于能够提供更多的控制。


## 16.5. 层次与Javascript ##

一个浏览器自动化工具本质上是建立在三个动态部件上:

* 询问DOM的一种方式
* 用于执行Javascript的机制
* 模拟用户输入的一些方法

本小节主要介绍第一个部分:提供一种询问DOM的机制。浏览器的通用语言是Javascript,所以它应该是一种理想的用来询问DOM的语言。虽然这个选择看上去是显然的,当考虑到Javascript的时候,却导致了一些有趣的挑战和需要平衡的竞争需求。

像大多数的大型项目一样,Selenium使用了分层结构的库。最底层是Google的Closure Library,它提供的原语和模块化机制允许源文件尽可能的小而集中。往上一层是一个实用函数库,提供了从简单的任务,比如获取一个属性的值,通过判断一个元素对某个终端用户是否可见,到复杂得多的一个动作,比如使用一个合成事件来模拟点击动作。在项目中,这些被视为提供了浏览器自动化的最小单位,因此被称为浏览器自动化原子(Browser Automation Atoms)或原子(atoms)。最后,为了满足WebDriver和Core的API,提供了一个整合了原子的适配器层。

^![](/cdn/images/aosabook/69.png)
| [图16.3:Selenium Javascript库的层次结构] |

选择Closure库是由于以下几个原因。最主要的原因是Closure的编译器理解这个库所使用的模块化技术,Closure的编译器是一个针对以Javascript为输出语言的编译器。“编译(Compilation) ”可以像决定文件依赖顺序、链接文件并漂亮的把它们打印出来一样简单,也可以像预先优化和删除死码(dead code)一样复杂。另一个不可否认的优点是,项目组做Javascript这一部分编码的部分成员非常熟悉Closure库。

由于询问DOM的需求所在,这个代码的“原子”库普遍地使用于整个项目中。对于RC和那些主要用Javascript编写的driver来说,这个库被直接的一般性的编译成了一个统一的脚本。对于用Java编写的driver来说,在WebDriver的适配器层的个别函数的编译被全面优化,生成的Javascript被当做资源包括进JAR包中。对于用C的变体写的driver,比如iPhone和IE的driver,不仅仅是个别的函数的编译被全面优化,生成的输出被转化为固定定义头,根据需求这个头部通过driver的一般Javascript执行机理运行。虽然看上去有点奇怪,但是它使得Javascript不需要在多个位置上暴露源码,就能压入底层driver中。

由于这些原子的广泛使用,不同浏览器之间行为的一致性可以被确保,且由于这些库使用Javascript便携的,不用提升权限就能执行开发周期,简单又快速。Closure库可以支持动态绑定,因此Selenium的开发者只需要编写一个测试脚本并加载到浏览器中,根据要求修改代码并点击刷新按键。一旦一个测试脚本在一个浏览器中通过测试,就很容易把它加载到别的浏览器中,并且一定能通过。由于Closure库在将浏览器之间的不同之处抽象出来的方面做得很好,虽然知道在每一个支持的浏览器上都持续的运行这些测试套件的构建是很令人安心的,但是这是往往不够的。

原始的Core和WebDriver有很多代码等同的部分,即在稍微不同的方式下实现了同样的功能的代码。当我们开始实现原子的时候,代码被梳理,试图找到“最佳”的功能。毕竟这两个项目都已经被广泛使用了,并且它们的代码都非常健壮,所以如果把两个项目都丢弃从头开始,是非常浪费且愚蠢的。由于每个原子都被提取了,在会被使用的每个点都被识别出来并转化为使用原子。比如,Firefox的driver中的getAttribute方法从大约50行的代码缩减到6行,包括空行:

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(); };
1
2
3
4
5
6
7
8
9
10
11
12

上面的第二行到最后一行,就是''respond.value''被分配到的地方,使用了原子的WebDriver库。

原子是这个项目的几个架构主题的一个实际演示。当然,他们倾向于用Javascript来实现API。更好的是,相同的库通过代码库来共享;当一个bug需要通过多个实现来检查并修正时,现在只需要在一个地方修正bug就够了,这减少了修改的成本,提高了稳定性和高效性。原子也让项目的巴士因素的优势增加了。当一个普通的Javascript单元测试脚本可以用来检查修改工作时,加入一个开源项目的障碍比起过去需要了解每个driver是如何实现的要减少很多。

使用原子还有一个好处。一个模仿了现有的RC实现,但是依赖于Webdriver的层次,对于在受控的情况下寻求迁移到新的WebDriver的API的方法的项目组来说是一个重要的工具。但是在Selenium Core原子化的情况下,可以从它来逐个编译每个函数,这让编写这个模仿层的任务变得更简单更精确。

不用说,采用这种方法也有缺点。最主要的是,把Javascript编译成C的''const''是一件非常奇怪的事,这常常阻挡了想要来写C的新的项目的贡献者。而且,拥有所有浏览器的每个版本,并且愿意把所有的测试脚本在这些浏览器上都运行一遍的开发者非常的稀少,一个可能发生的情况是,有人不小心在一个意想不到的地方引起了复原事件,需要花一些来发现这个问题,特别是当一个连续的构建正处于片状时。

由于原子照标准在浏览器之间返回值,但还是有可能返回一些意料之外的值。比如,这段HTML代码:

```<input name="example" checked>

‘’checked’’属性的值依赖于使用的浏览器。原子标准化了这一点,还有其他定义在HTML5规范中的布尔属性,只能是”true”或“false”。当这个原子被引入代码库时,我们发现很多地方,人们都在做关于返回值类型应该是什么的浏览器相关的假设。当一个值被固定下来时,我们将要花费很长时间来向大家解释发生了什么,为什么设置这个值。

16.6. 远程的driver,特别是Firefox的driver

远程的WebDriver本来是一个众人称赞的RPC机制。自从我们引进了一个关键的机制,用于减少WebDriver的维护开销,通过提供一个语言绑定件都能编写的统一的接口。尽管我们已经尽可能的把逻辑从语言绑定件中抽取出来,放在driver中,但是当每个driver需要通过一个特殊的协议进行交流时,在语言绑定件中依然需要重复很多的代码。

当我们需要与运行在程序外的一个浏览器实例进行交流时,都需要使用远程WebDriver协议。设计这个协议需要考虑多方面,大多数是技术方面的问题,但是对于一个开源软件,还需要考虑社会层面上的问题。

任何的RPC机制都被分为两个部分:传输和解码。我们知道,无论我们怎么实现远程WebDriver协议,我们作为客户端需要在每个我们使用的语言上都支持这两个方面。第一次设计是作为Firefox driver的一部分开发的。

Mozilla,也包括Firefox的,总是被看作是由它们的开发者所提供多平台的应用程序。为了便于开发,Mozilla创建了一个由Microsoft的COM所启发而成的一个框架,COM由于允许组件和被构建和螺栓连接在一起而被称为XPCOM(跨平台的,COM)。一个XPCOM接口使用IDL声明,并且有C和Javascript语言以及其他语言的语言绑定件。由于XPCOM用于构造火狐,又因为XPCOM有javascript绑定,可以利用XPCOM对象对Firefox扩展。

普通的Win32 COM允许接口被远程访问。也有计划将这个功能添加到XPCOM,达林·费舍尔增加了一个XPCOM ServerSocket的实施促成了此功能的实现。虽然D-XPCOM计划没有能够实现,但他像一个附录中,残留的基础设施仍然存在。我们注意到这一优势,并在包含所有控制Firefox的逻辑的Firefox定制扩展中,创建了一个非常基本的服务器。使用的协议最初是基于文本和面向行为主,所有的字符串为UTF-2编码。每个请求或响应开始与一个数字,表明在得出请求或应答已发送的结论之前,有多少新行需要记录。至关重要的是,该方案很容易在Javascript实现,因为SeaMonkey(当时Firefox的Javascript引擎)把JavaScript字符串作为内部16位无符号整数存储。

虽然把玩原始套接字上的自定义编码协议是用来打发时间的一个有趣的方式,但它有几个缺点。自定义的协议没有广泛可用的库,所以它是需要从基层构建起来的,而且是我们希望支持每个语言都要实现一次。这个增加编写的代码的要求,将不太可能让较多的开源贡献者参与新的语言绑定的开发。此外,虽然面向行的协议是很好,但当我们只发送关于基于文本的数据的时候,它将会在我们想发送图像(如屏幕截图)之类的时候带来的问题,

这个最初的RPC机制很明显很快的就被认为是不实际的。幸运的是,有一个著名的运输是几乎在每一种语言的广泛采用和支持我们想做什么就做什么那就是:HTTP。

一旦我们决定使用HTTP作为输送机制,也可以提出,下一步需要选择的是,是否使用单一终点(SOAP)或多个端点(REST中的样式)。原Selenese的协议使用单一的终点和在查询字符串有编码的命令和参数。尽管这种方法效果很好,但是“感觉”不对:我们有一个能够在浏览器中连接到远程的webdriver实例,以便于查看服务器状态的愿景。我们最终选择了我们称之为“REST-ish”的方法:使用HTTP的动词来帮助提供,这意味着多个端点的URL,但打破真正的RESTful系统所需的诸多约束,特别是围绕状态和高速缓存能力的定位,主要是因为只有一个定位能够使得该应用程序的存在有意义。

虽然HTTP可以轻松的支持基于内容类型协商的多种编码数据的方法,我们认为我们需要一个远程的Webdriver协议的实现能够一起工作的规范形式。很明显我们能选择的不多:HTML,XML或JSON。我们很快排除了XML:虽然这是一个完全合理的数据格式,并且几乎每一个语言都有支持它的库,我对于它在开源社区中是否受欢迎的看法是,人们并不喜欢使用它。此外,虽然返回的数据将共用一个共同的“形状”但要添加附加字段是很容易的,而且是完全有可能的。虽然这些扩展可以用XML命名空间来建模,这将开始给客户端代码引入更多的复杂性:这种事情是我们极力避免。 因此XML是一种被放弃了的选择。 HTML也真的不是一个很好的选择,因为我们需要能够定义我们自己的数据格式,虽然嵌入式微格式可能已经被设计出来,并能够像用锤子来敲鸡蛋一样的使用。

最后一种可能的选择是Javascript Object Notation(JSON)。浏览器可以将字符串转换成一个对象,通过两种方式:直接调用EVAL对象;或者,在最新的浏览器上,可以用最原始的设计来把一个JavaScript对象转化为一个字符串,或反过来转化,安全又无副作用。从实用的角度来看,JSON是一种流行的数据格式,拥有可用于处理几乎所有的语言的库,时尚的年轻人都喜欢使用。一个简单的选择。

因此,第二代远程WebDriver的协议使用HTTP,因为HTTP的翻译机制和用UTF-8作为默认编码方案对JSON编码。UTF-8被选为默认编码方式,使客户可以很容易地用对Unicode的支持有限的语言编写,因为UTF-8与ASCII向后兼容。发送到服务器的命令使用URL来确定要发送哪些命令,为数组中的命令编码参数。

例如一个’’WebDriver.get(“http://www.example.com “)’’的调用对应于一个POST请求的URL编码会话ID并以“/url”结尾,有一个像是{[}’http://www.example.com ‘{]}的属性数组。返回的结果是一个较为结构化,并有占位符的返回值和错误代码。不就之后,远程协议的第三次迭代出现,它取代了参数要求的阵列命名参数的字典。这有使调试请求变得显著容易的优点,并除去客户误错序参数的可能性,使得系统作为一个整体更健壮。当然,决定使用正常的HTTP错误代码,以表明最合适的方式的一定的返回值和回应;例如,如果用户试图调用一个无任何映射到的URL时,或者当我们想表明“空响应”时。

远程的WebDriver协议具有两层错误处理,一个用于无效的请求,和一个用于失败的命令。无效的请求的一个例子,对于未在服务器上的资源,或者该资源不理解的动词(例如发送一个DELETE命令到用于处理当前页面的URL中的资源上)。在这种情况下,一个正常的HTTP的4xx响应被发送。对于失败的命令,响应错误代码为500(“内部服务器错误”),并返回的数据中包含的什么地方出了错的更详细的描述。

当一个响应包含从服务器发送的数据,它需要一个JSON对象的形式:

关键词描述

sessionId:服务器所使用的不透明句柄来确定路由会话特定的命令。Status:数字状态代码总结命令的结果。非零值表明命令失败。value响应JSON值。一个响应的例子:

1
{ sessionId: 'BD204170-1A52-49C2-A6F8-872D127E7AE8', status: 7, value: 'Unable to locate element with id: foo' }

如你所见,我们在响应中进行状态码的编码,用一个表示某物已经可怕出差错的一个非零值。 IE的driver是第一次使用状态码,并在有线协议中使用的值反映这些。因为所有错误代码在driver之间是一致的,所以在所有用一个特定的语言辨析的驱动器之间可以共用错误处理码,这使得客户端实施者工作更简单。

远程服务器的WebDriver简直是一个Java servlet充当多路复用器,路由,接收到合适的webdriver实例的任何命令。它的那种,一个二年级研究生可以写的东西。 Firefox的驱动程序还实现了远程webdriver的协议,它的结构更加有趣,让我们通过跟着请求从语言绑定到后端调用,直到它返回给用户。

假设我们使用的是Java,而“元素”是WebElement的一个实例,这一切从这里开始:

1
element.getAttribute("row");

在内部,元素具有不透明“ID”,服务器端用以确定我们正在谈论哪个元素。为了讨论的方便,我们会想象它的值为“some_opaque_id”。这被编码成一个带有’’Map’’的Java ‘’Command’’ 对象持有(现名为)参数’’id’’用于元素ID,参数’’name’’用于被查询的属性的名称。

表格中的快速查找表明了正确的URL是:

1
/session/:sessionId/element/:id/attribute/:name

假设以冒号开始URL的任何部分是一个需要替换的变量。我们已经被赋予了’’id’’和’’name’’参数,并且’’sessionId’’是用于当一台服务器可以同时处理多个会话(其中Firefox的驱动程序不能)时,进行路由的另一种不透明的句柄。此URL通常因此扩展为类似于:

1
http://localhost:7055/hub/session/XXX/element/some_opaque_id/attribute/row

顺便说一句,WebDnriver的远程有线协议最初是与URL模板作为一个RFC草案被提出的同一时间开发的。两个我们指定的URL和URL模板的方案允许变量在URL中进行扩展(因此而得)。可悲的是,虽然URL模板在同一时间提出,但我们在当天较晚时才意识到他们之间的联系,因此它们不是用来形容有线协议。

因为我们执行的方法是幂等[4],HTTP的正确的使用方法是GET。我们委托一个Java库,来处理HTTP(Apache的HTTP客户端)调用服务器。

^
| [图16.4:Firefox的驱动程序体系结构概述] |

FireFox的driver被实现为Firefox扩展,其中在图16.4中展示出的基本设计,有点不同寻常,它具有一个嵌入式HTTP服务器。虽然最初我们使用的是一个我们自己已经建立的,写XPCOM的HTTP服务器是不是我们的核心竞争力之一,所以当机会出现,我们用由Mozilla自己写的一个基本的HTTPD取而代之。请求被HTTPD接受后,几乎马上传递给一个’’dispatcher’’对象。

调度员接管支持的一个已知的URL列表的请求和迭代,试图找到一个能匹配请求的URL。该匹配是通过在客户端的变量插值的知识完成的。一旦找到精确匹配,包括动词使用,一个表示了要执行的命令的JSON对象被构造出来。在我们的例子中,它看起来像:

1
{ 'name': 'getElementAttribute', 'sessionId': { 'value': 'XXX' }, 'parameters': { 'id': 'some_opaque_key', 'name': 'rows' } }

这是那么作为一个JSON字符串到我们已经编写并命名为CommandProcessor的自定义XPCOM组件的过度。代码如下:

1
2
3
4
5
var jsonResponseString = JSON.stringify(json);
var callback = function(jsonResponseString) { var jsonResponse = JSON.parse(jsonResponseString);
if (jsonResponse.status != ErrorCode.SUCCESS) { response.setStatus(Response.INTERNAL_ERROR); }
response.setContentType('application/json'); response.setBody(jsonResponseString); response.commit(); };
* Dispatch the command. Components.classes['@googlecode.com/webdriver/command-processor;1']. getService(Components.interfaces.nsICommandProcessor). execute(jsonString, callback);

这里的代码相当多,但其中有两个关键点。首先,把上面的一个对象转换为一个JSON字符串。其次,传递一个回调到出发HTTP响应发送excute方法。

命令处理器的Execute方法查找“名称”,以确定调用哪个函数,它然后执行。给这个实施函数的第一个参数是一个“’’respond’’”的对象(这么命名是因为它原来只有用于将响应发送回给用户的功能),它不仅封装了可能被发送的可能的值,而且还具有允许响应被回填给用户和机制,以找到DOM的信息。第二个参数是上面看到的’’parameters’’对象的值(在上面的例子中是’’id’’和’’name’’)。这个方案的优点是,每个功能具有一个统一的接口,对应了在客户端使用的结构。这意味着,用于考虑每一侧代码中的思维模型是相似的。这里是’’getAttribute’’的底层实现,已在16.5节看到过:

1
2
3
4
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(); };

为了使元件的引用一致,第一行简单地查找由不透明的ID在一个高速缓存中提到的元件。在Firefox的driver中,不透明的ID是一个UUID,“告诉缓存”是一个简单的映射。

该’’getElementAt’’方法还检查是否被引用的元件都已知并且附加到DOM。如果任何检查失败,ID从缓存中删除(如果需要),并抛出一个异常返回给用户。

倒数第二行利用前面讨论过的浏览器自动化的原子,此时编译为一个单一脚本并加载作为扩展的一部分。

在最后一行,send方法被调用。这确实一个简单的检查,以确保在它调用提供给执行方法的回调之前,只’’send’’一个响应一次。该响应以一个JSON字符串的形式被发送回用户,它注入一个对象,看起来像这样(假设’’getAttribute’’返回“7”,这意味着该元素未发现):

1
{ 'value': '7', 'status': 0, 'sessionId': 'XXX' }

Java客户端接着检查状态字段的值。如果该值不为零,它的数值状态代码转换为正确类型的异常并抛出,使用“value”字段帮助设置向用户发送的消息。如果状态是零,“value”字段的值被返回给用户。

大多数这使得一定的意义,但有一件一个精明的读者都会提出的问题:为什么调度员在调用execute方法之前将它有的对象转换成一个字符串?

这样做的原因是,Firefox Driver也支持运行用纯的Javascript编写的测试。通常情况下,这将是一个非常难以支持的事情:测试都是在浏览器的JavaScript安全沙箱的上下文中运行,因此可能不能做一系列在测试中有用的事情,如在域或上传文件之间切换。WebDriver的Firefox扩展,于是提供了从沙盒的逃脱的窗口。它通过添加一个’’webdriver’’属性到文档元素宣布了它的存在。WebDriver的Javascript API使用这个作为一个指标,用于添加JSON序列化的命令对象作为文档元素上的’’commad’’属性的值,触发一个自定义的’’webdriverCommand’’事件,和监听同样的元素的’’webdriverResponse’’事件,它在’’response’’属性被设置的时候会被通知。

这表明,在安装了WebDriver扩展件的Firefox的副本中,浏览网页是一个非常糟糕的主意,因为它使得随便一个人都可以很轻松的远程控制浏览器。

在幕后,有一个DOM传信者,等待’’webdriverCommand’’读取序列化的JSON对象,并调用命令处理器的Execute方法。这时候,回调是一个简单的设置文档元素的’’response’’ 属性,然后触发预期’’webdriverResponse’’事件。

翻译参考文献

[1] http://www.ituring.com.cn/article/16152 |卷1:第16章 Selenium WebDriver 作者: http://www.ituring.com.cn/users/56841 |NullPointer

[2] http://www.infoq.com/cn/news/2011/06/selenium-arch |开源应用架构之?Selenium WebDriver(上) 作者: http://www.infoq.com/cn/author/%E5%B4%94%E5%BA%B7 |崔康