(实战)通过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对象中,那么自动化测试代码是:

(实战)通过WebElement.sendKeys()来研究wire协议

sendKeys()方法如下:

(实战)通过WebElement.sendKeys()来研究wire协议

当调用sendKeys方法的时候,它会吧我们要输入的内容值转为一个字符数组,这个很好理解,因为任何字符串都是一组键盘的输入的集合。所以我们的policyName的值被转为:

(实战)通过WebElement.sendKeys()来研究wire协议

然后,它去在87行调用execute()方法,并且使用了Command设计模式,把我们的调用sendKeys(String) 方法名转为了一个命令DriverCommand.SEND_KEYS_TO_ELEMENT(也就是字符串 “sendKeysToElement”),然后把我们要发送的内容keysToSend变量通过ImmutableMap进行打包, 这样做的目的是为了让我们的输入的内容不可改变。

因为在我们例子中,我们使用的是Linux操作系统上的Firefox所以,它会调用FirefoxWebElement上的execute()方法,并且我们的输入内容中被ImmutableMap包装后加上了WebElementid

(实战)通过WebElement.sendKeys()来研究wire协议

然后它接着会调用父类的execute()方法来完成这个操作:

(实战)通过WebElement.sendKeys()来研究wire协议

这是最重要的方法,我们仔细分析:

从宏观上看,首先,在第436行会吧当前WebDriver到它启动的浏览器的所创建的session对应的sessionId,以及命令字符串(也就是上文中传递过来的DriverCommand.SEND_KEYS_TO_ELEMENT字符串常量),还有发送的字符串内容的包装体,都封装在一个Command对象中,封装后这个Command对象如下:

(实战)通过WebElement.sendKeys()来研究wire协议

值得一提的是:这个sessionIdWebDriver每次启动浏览器时候分配的唯一的会话id,从而保证多线程并行运行时候不会出现问题,而总是吧请求发送到正确的浏览器所包含的webservice中。(关于这一点,我们在精华分析1中会讲到)


然后,它会在446行用CommandExecutorexecute()方法来执行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()方法如下

(实战)通过WebElement.sendKeys()来研究wire协议

而startSession会在开始就用Command模式,调用execute()方法创建一个新的session:

(实战)通过WebElement.sendKeys()来研究wire协议

而这个execute方法,最终被HttpCommandExecutor来执行,和前面叙述一样,它会发送一个Http请求,并且所有请求细节都在Command对象中。可以从下面调试信息看到,Command是如下的信息:

(实战)通过WebElement.sendKeys()来研究wire协议

它的命令name是newSession,而sessionId为空,因为还没有创建嘛。

然后info对象中包含了要发送的请求url,这里可以看出,它发送到的请求url是/session

(实战)通过WebElement.sendKeys()来研究wire协议


最后从httpMethod对象中,可以看出httpMethod用的是HttpPost

(实战)通过WebElement.sendKeys()来研究wire协议

所以联系以上的信息就知道,在RemoteWebDriver中,其实它是以HttpPost方法发送了一个请求对象到/session中,并且请求对象中包含了命令”newSession”还有一些desiredCapabilities信息。

我们对比wire 协议:

(实战)通过WebElement.sendKeys()来研究wire协议

正如协议中描述的,这个请求是用来创建一个新的session的,我们检查参数,请求类型,请求payload完全一致。

所以最后会发送此请求,发送完的response中会包含新创建的sessionId.

(实战)通过WebElement.sendKeys()来研究wire协议

然后这个sessionId就可以作为每次发送请求到的目标浏览器的标识,从而保证每次请求的都是正确的浏览器了,当然,这个sessionId就必须被包含在每次请求中。

精华分析2:HttpCommandExecutor执行命令的细节.

我们看下HttpCommandExecutor.execute()方法:

(实战)通过WebElement.sendKeys()来研究wire协议

首先,它会在第279行吧command的名字(命令名,也就是我们的DriverCommand.SEND_KEYS_TO_ELEMENT)转为一个url形式的命令。回想,我们用的是REST ,所以命令也要用路径表达式的方式表现出来,转换后,CommandInfo如下:

(实战)通过WebElement.sendKeys()来研究wire协议

所以sendKeysToElement 命令被转为POST /session/:sessionId/element/:id/valueurl形式。

我们对比Wire协议的说明:

(实战)通过WebElement.sendKeys()来研究wire协议

所以,这里我们转换对了,的确我们sendKeysToELement的最终目的是发送一组键盘敲击动作序列到指定元素。

然后,它在第281行从刚才的CommandInfo对象中分离出Http动作:

(实战)通过WebElement.sendKeys()来研究wire协议

并且这个getMethod方法内部还会吧我们的url中由名字参数(:sessionId),(:id)表示的url全部替换为真实值,并且前面拼接上由remoteServer实例变量指定的服务器请求url

因为从调试信息上看,info中的动词(verb)的名字叫”POST”,所以它最终会被转为httpMethodHttpPost。而这个uri被变量具体化后被转为:

(实战)通过WebElement.sendKeys()来研究wire协议

这里可以看出(:sessionId),(:id)都被替换了,其中sessionId来自于浏览器的sessionId,具体可参见精华分析1.


然后在283行吧HttpPost设置为Http Accept头。

接着根据不同的HttpMethod进行不同的处理,因为我们的请求是httpPost 请求,所以它会在第286行利用BeanToJsonConverter()吧我们封装在Command对象中的内容转成json格式的payload ,并且接下来设置payload的编码格式以及Content-Type内容。

(实战)通过WebElement.sendKeys()来研究wire协议

转换之后的json变为:

(实战)通过WebElement.sendKeys()来研究wire协议

最后,在297行通过调用fallbackExecute来发送浏览器中,从这里可以看出,这个的确是一个RESTfulWeb Service调用。

(实战)通过WebElement.sendKeys()来研究wire协议

当处理完之后,其结果封装在HttpResponse对象中,我们要对它进行后处理,从调试信息看,这个Response是一个标准的HttpResonse

(实战)通过WebElement.sendKeys()来研究wire协议

我们发现了一个很有趣的东西,这里发现这个serverhttpd.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的代码中。



原创文章,作者:carmelaweatherly,如若转载,请注明出处:https://blog.ytso.com/tech/opensource/197069.html

(0)
上一篇 2021年11月17日 02:28
下一篇 2021年11月17日 02:28

相关推荐

发表回复

登录后才能评论