(实战)通过WebElement.sendKeys()来研究wire协议
引入:
其实熟悉selenium的人肯定都对wire协议不陌生,因为我们知道,当我们在代码中使用WebDriver API 做一些操作的时候,它最终会转为一个基于wire协议的命令(Command)发送到浏览器,并且请求的内容都封装在json对象中,通过WebService调用浏览器,从而所有WebDriver API的调用都最后转为对浏览器的Web Service调用。
我们这里就通过最简单的输入文本内容(WebElement.sendKeys(String))来研究下wire协议。以及上述所有细节。
Wire 协议的参考规范如下:
http://code.google.com/p/selenium/wiki/JsonWireProtocol
粗略看了下,这个wire协议可强大了,几乎可以操作自然人对浏览器能做的任何事情,比如打开,关闭,点击,关闭,定位,上传文件,最大最小化等等。
它是一套基于RESTful风格的web service.
调试实战:
比如说页面上有个输入框id叫那么叫 policy-name ,然后我们要输入的值在dataProvider对象中,那么自动化测试代码是:
sendKeys()方法如下:
当调用sendKeys方法的时候,它会吧我们要输入的内容值转为一个字符数组,这个很好理解,因为任何字符串都是一组键盘的输入的集合。所以我们的policyName的值被转为:
然后,它去在87行调用execute()方法,并且使用了Command设计模式,把我们的调用sendKeys(String) 方法名转为了一个命令DriverCommand.SEND_KEYS_TO_ELEMENT(也就是字符串 "sendKeysToElement"),然后把我们要发送的内容keysToSend变量通过ImmutableMap进行打包, 这样做的目的是为了让我们的输入的内容不可改变。
因为在我们例子中,我们使用的是Linux操作系统上的Firefox所以,它会调用FirefoxWebElement上的execute()方法,并且我们的输入内容中被ImmutableMap包装后加上了WebElement的id。
然后它接着会调用父类的execute()方法来完成这个操作:
这是最重要的方法,我们仔细分析:
从宏观上看,首先,在第436行会吧当前WebDriver到它启动的浏览器的所创建的session对应的sessionId,以及命令字符串(也就是上文中传递过来的DriverCommand.SEND_KEYS_TO_ELEMENT字符串常量),还有发送的字符串内容的包装体,都封装在一个Command对象中,封装后这个Command对象如下:
值得一提的是:这个sessionId是WebDriver每次启动浏览器时候分配的唯一的会话id,从而保证多线程并行运行时候不会出现问题,而总是吧请求发送到正确的浏览器所包含的webservice中。(关于这一点,我们在精华分析1中会讲到)
然后,它会在446行用CommandExecutor的execute()方法来执行Command从而吧命令发送到sessionId指定的浏览器内含的web service服务中,最终它使用HttpCommandExecutor来完成这个任务
(执行命令的细节,是我们所探索的最主要目的,它反映了基于wire协议的web service调用,这点我们在精华分析2中讲到)
最后,在第455-456行对于执行结果的返回进行一些后处理,于是你就可以在页面上看到自动化测试的动画了。
精华分析1:sessionId是如何产生的?
因为我们使用的是Firefox浏览器做的测试(其他浏览器也一样),当WebDriver启动浏览器的时候,它会调用webDriver = new FirefoxDriver(firefoxBinary,firefoxProfile)方法,
因为FirefoxDriver继承自RemoteWebDriver,所以调用FirefoxDriver构造器时候会调用RemoteWebDriver的构造器,其最后一行会调用startSession()方法如下:
而startSession会在开始就用Command模式,调用execute()方法创建一个新的session:
而这个execute方法,最终被HttpCommandExecutor来执行,和前面叙述一样,它会发送一个Http请求,并且所有请求细节都在Command对象中。可以从下面调试信息看到,Command是如下的信息:
它的命令name是newSession,而sessionId为空,因为还没有创建嘛。
然后info对象中包含了要发送的请求url,这里可以看出,它发送到的请求url是/session
最后从httpMethod对象中,可以看出httpMethod用的是HttpPost
所以联系以上的信息就知道,在RemoteWebDriver中,其实它是以HttpPost方法发送了一个请求对象到/session中,并且请求对象中包含了命令"newSession"还有一些desiredCapabilities信息。
我们对比wire 协议:
正如协议中描述的,这个请求是用来创建一个新的session的,我们检查参数,请求类型,请求payload完全一致。
所以最后会发送此请求,发送完的response中会包含新创建的sessionId.
然后这个sessionId就可以作为每次发送请求到的目标浏览器的标识,从而保证每次请求的都是正确的浏览器了,当然,这个sessionId就必须被包含在每次请求中。
精华分析2:HttpCommandExecutor执行命令的细节.
我们看下HttpCommandExecutor.execute()方法:
首先,它会在第279行吧command的名字(命令名,也就是我们的DriverCommand.SEND_KEYS_TO_ELEMENT)转为一个url形式的命令。回想,我们用的是REST ,所以命令也要用路径表达式的方式表现出来,转换后,CommandInfo如下:
所以sendKeysToElement 命令被转为POST /session/:sessionId/element/:id/value的url形式。
我们对比Wire协议的说明:
所以,这里我们转换对了,的确我们sendKeysToELement的最终目的是发送一组键盘敲击动作序列到指定元素。
然后,它在第281行从刚才的CommandInfo对象中分离出Http动作:
并且这个getMethod方法内部还会吧我们的url中由名字参数(:sessionId),(:id)表示的url全部替换为真实值,并且前面拼接上由remoteServer实例变量指定的服务器请求url
因为从调试信息上看,info中的动词(verb)的名字叫"POST",所以它最终会被转为httpMethod为HttpPost。而这个uri被变量具体化后被转为:
这里可以看出(:sessionId),(:id)都被替换了,其中sessionId来自于浏览器的sessionId,具体可参见精华分析1.
然后在283行吧HttpPost设置为Http Accept头。
接着根据不同的HttpMethod进行不同的处理,因为我们的请求是httpPost 请求,所以它会在第286行利用BeanToJsonConverter()吧我们封装在Command对象中的内容转成json格式的payload ,并且接下来设置payload的编码格式以及Content-Type内容。
转换之后的json变为:
最后,在297行通过调用fallbackExecute来发送浏览器中,从这里可以看出,这个的确是一个RESTful的Web Service调用。
当处理完之后,其结果封装在HttpResponse对象中,我们要对它进行后处理,从调试信息看,这个Response是一个标准的HttpResonse
我们发现了一个很有趣的东西,这里发现这个server是httpd.js,这就说明,其实真正消费我们Http请求的是浏览器内置的一段httpd.js的脚本,这也和我们理论模型(浏览器包含了一段js来专门处理基于wire协议的请求)完全符合,可以猜想这段js就是模拟输入值到输入框中的动画。
我在selenium官网找到了这个js文件,其内容在:http://code.google.com/p/selenium/source/browse/firefox/src/extension/components/httpd.js?spec=svn004f447f8b359859da694f79569d7e5b03470dd7&r=004f447f8b359859da694f79569d7e5b03470dd7
当我们拿到Response对象后,我们要进行后处理,我们后处理不感兴趣,就不分析了。
总结:
从这里我们可以获取许多有用的信息
(1)从架构的角度来看,当我们用WebDriver API 调用来书写自动化测试的代码时候,最终这些方法调用都会被selenium框架内部转为一个基于wire协议的web service调用。采用的设计模式是Command模式,这个web service的服务端是在任何浏览器中都包含的,并且用于服务的其实是httpd.js这段代码。
(2)wire协议几乎可以模拟自然人对浏览器能做的任何事情,比如打开,关闭,点击,关闭,定位,上传文件,最大最小化等等。它是一套基于RESTful风格的web service.
(3)每次web service调用的时候,都必须有一个sessionId作为请求url一部分,这个sessionId用于唯一标识请求要送到的浏览器,并且是唯一的uuid,从而保证在多线程环境中工作的正确性。它的产生在于初始化浏览器的WebDriver时候,会发送一个Command为newSession的web service到浏览器中,这个请求路径是/session,并且payload中包含了目标浏览器的desireCapability信息,这样,这个web service的调用就会返回一个sessionId,然后包含在后续的所有操作中。
(4)在具体执行某个API 调用,比如sendKeys,它会转为web service的调用,调用的url会包含sessionId和其他一些相关信息,以名字参数的形式,而调用过程开始时,这些名字参数会被代替为实际参数。然后Command中封装的所有参数信息会被转为一个json对象作为web service的payload , 请求类型也会根据你的实际请求动作的需要指定,最后请求调用的过程就是一个web service调用的过程。它的服务端被各种浏览器实现,并且包含在一段叫httpd.js的代码中。