Selenium
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命名的元素标示符:
1 |
|
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映射是一种能够期望它的方法能够有效的接口。
^
| [图16.1:基于Shop的Accountant和Stockist] |
^
| [图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,提供了一个整合了原子的适配器层。
^
| [图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 |
|
‘’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 | var jsonResponseString = JSON.stringify(json); |
这里的代码相当多,但其中有两个关键点。首先,把上面的一个对象转换为一个JSON字符串。其次,传递一个回调到出发HTTP响应发送excute方法。
命令处理器的Execute方法查找“名称”,以确定调用哪个函数,它然后执行。给这个实施函数的第一个参数是一个“’’respond’’”的对象(这么命名是因为它原来只有用于将响应发送回给用户的功能),它不仅封装了可能被发送的可能的值,而且还具有允许响应被回填给用户和机制,以找到DOM的信息。第二个参数是上面看到的’’parameters’’对象的值(在上面的例子中是’’id’’和’’name’’)。这个方案的优点是,每个功能具有一个统一的接口,对应了在客户端使用的结构。这意味着,用于考虑每一侧代码中的思维模型是相似的。这里是’’getAttribute’’的底层实现,已在16.5节看到过:
1 | FirefoxDriver.prototype.getElementAttribute = function(respond, parameters) { |
为了使元件的引用一致,第一行简单地查找由不透明的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 |崔康