开始菜单--》HP LoadRunner-->applications--->virtual user Generator
1>新建--》类别 (选择协议):常用的
web项目用Web(HTTP/HTML),后台运行的服务,选择
java下的java vuser,编写java代码;
2>对于web,需要设置访问的URL地址,录制到的操作:init action end,默认是action,
2.1确定后会弹出浏览器并打开刚设置的URL,而且弹出了录制工具条,此时已经进入了录制过程;
2.2录制完一个操作(比如录制输入用户名、密码后的登录操作),可以在那个录制的工具条上新建一个新的;
2.3如果要停止录制,先关闭浏览器,然后点击录制工具条上的停止按钮,此时脚本文件就有了代码;
2.4修改脚本,比如录制的登录过程,可能需要登录多个用户,那就找到脚本里设置登录用户名(可以查刚才录制时登录的用户名)的地方,
选中该用户名,右键--》替换为参数--》设置参数名称--》点击属性按钮-->创建表--》添加行--》输入被
测试系统已有的测试用户名;
对话框下面的“更新值的时间”,有三个选择,请根据具体测试情景选择,然后“关闭”--》“确定”;
2.5此时刚才的用户名会变成参数形式,如果有多个地方需要用这个参数替换,则找到这些地方选中,右键---》“使用现有参数”,找到要替换的参数;
2.6点击工具栏的“运行”按钮,会弹出一个结果概要的窗口,显示脚本运行是否通过或者报错;
2.7给脚本设置具体的场景:
VUGen 工具---》创建controller场景---》设置load Generator(
loadrunner所在IP)弹出controller界面:最下方分出 “设计”和“运行”两种选项卡;
运行前的配置操作在“设计”选项卡操作,常用的如下:
1》场景组里可以设置多个组,组里的Load Generator可以是当前loadrunner或另外一个loadrunner 客户端(可以只安装load Generator),相当于是负载,安装了controller的lr可以作为中心,配置它的客户端方式为:
controller-->场景-->Load Generator-->添加(别的loadrunner 客户端)--》连接;
2》场景计划面板,如果计划方式选择的是场景,那么下面的“全局计划”的设置是对所有组的,如果计划方式是组,那么下面是针对各组的设置计划;
3》“全局计划”面板,可以选中某一个,如“启动Vuser”双击,设置并发user数,是同时运行还是隔多长时间运行一次;
2.8设置完后,点最下方的“运行”选项卡,点右上方的“开始场景”运行,运行结束后该按钮由置灰变可点;
2.9右边的“场景状态”会显示运行情况,具体的分析报告可点菜单“工具”--》“analysis”弹出具体的分析面板,右边的概要报告会显示大概的指标;
3.0如果左边的会话浏览器里显示的图不够,可以点右键--》添加新项--->添加新图;
3、如果新建后选择的类别是 java vuser
3.1配置运行java脚本需要的java环境:Vuser->运行时设置-->java环境设置-->java VM 勾选“使用指定JDK”,并把jdk(lr11建议用32位的jdk)的bin上一层路径复制到JDK后,点确定;
3.2导入java环境的jar包:Vuser->运行时设置-->java环境设置-->Classpath,点添加jar的按钮,选择指定的jdk下的lib下的dt.jar和tools.jar导入;
3.3导入编写的java脚本需要依赖的jar:Vuser->运行时设置-->java环境设置-->Classpath,点添加jar的按钮,将编写脚本依赖的工程里的Jar包导入,点确定;
3.4此时可以编写java脚本,记住import类,和eclipse的写法一致就能找到类;
3.5运行脚本,看能否运行通过;
某些配置的具体操作方式:
1、VUGen 工具--》录制选项--》HTTP属性 --》高级 (设置字符集)
2、脚本设置:
2.1、VUGen 工具--》录制选项--》常规--》录制 (默认web使用html)
3、运行时的设置:
VUGen VUSER-->运行时设置--》运行逻辑 (设置运行的迭代次数)
VUGen VUSER-->运行时设置--》常规--》思考时间--》忽略思考时间 (将录制操作过程中的停顿忽略)
4、如果需要操作到达某个点时需要等待其他用户都到达这个点再同时向下执行,在整理代码时,可以在这个点上执行“插入--》集合”;
5、当需要请求的用户来自不同IP时,需要设置Controller的“场景”--》“启用IP欺骗器”;
开始菜单--》HP LoadRunner-->applications--->virtual user Generator
1>新建--》类别 (选择协议):常用的
web项目用Web(HTTP/HTML),后台运行的服务,选择
java下的java vuser,编写java代码;
2>对于web,需要设置访问的URL地址,录制到的操作:init action end,默认是action,
2.1确定后会弹出浏览器并打开刚设置的URL,而且弹出了录制工具条,此时已经进入了录制过程;
2.2录制完一个操作(比如录制输入用户名、密码后的登录操作),可以在那个录制的工具条上新建一个新的;
2.3如果要停止录制,先关闭浏览器,然后点击录制工具条上的停止按钮,此时脚本文件就有了代码;
2.4修改脚本,比如录制的登录过程,可能需要登录多个用户,那就找到脚本里设置登录用户名(可以查刚才录制时登录的用户名)的地方,
选中该用户名,右键--》替换为参数--》设置参数名称--》点击属性按钮-->创建表--》添加行--》输入被
测试系统已有的测试用户名;
对话框下面的“更新值的时间”,有三个选择,请根据具体测试情景选择,然后“关闭”--》“确定”;
2.5此时刚才的用户名会变成参数形式,如果有多个地方需要用这个参数替换,则找到这些地方选中,右键---》“使用现有参数”,找到要替换的参数;
2.6点击工具栏的“运行”按钮,会弹出一个结果概要的窗口,显示脚本运行是否通过或者报错;
2.7给脚本设置具体的场景:
VUGen 工具---》创建controller场景---》设置load Generator(
loadrunner所在IP)弹出controller界面:最下方分出 “设计”和“运行”两种选项卡;
运行前的配置操作在“设计”选项卡操作,常用的如下:
1》场景组里可以设置多个组,组里的Load Generator可以是当前loadrunner或另外一个loadrunner 客户端(可以只安装load Generator),相当于是负载,安装了controller的lr可以作为中心,配置它的客户端方式为:
controller-->场景-->Load Generator-->添加(别的loadrunner 客户端)--》连接;
2》场景计划面板,如果计划方式选择的是场景,那么下面的“全局计划”的设置是对所有组的,如果计划方式是组,那么下面是针对各组的设置计划;
3》“全局计划”面板,可以选中某一个,如“启动Vuser”双击,设置并发user数,是同时运行还是隔多长时间运行一次;
2.8设置完后,点最下方的“运行”选项卡,点右上方的“开始场景”运行,运行结束后该按钮由置灰变可点;
2.9右边的“场景状态”会显示运行情况,具体的分析报告可点菜单“工具”--》“analysis”弹出具体的分析面板,右边的概要报告会显示大概的指标;
3.0如果左边的会话浏览器里显示的图不够,可以点右键--》添加新项--->添加新图;
3、如果新建后选择的类别是 java vuser
3.1配置运行java脚本需要的java环境:Vuser->运行时设置-->java环境设置-->java VM 勾选“使用指定JDK”,并把jdk(lr11建议用32位的jdk)的bin上一层路径复制到JDK后,点确定;
3.2导入java环境的jar包:Vuser->运行时设置-->java环境设置-->Classpath,点添加jar的按钮,选择指定的jdk下的lib下的dt.jar和tools.jar导入;
3.3导入编写的java脚本需要依赖的jar:Vuser->运行时设置-->java环境设置-->Classpath,点添加jar的按钮,将编写脚本依赖的工程里的Jar包导入,点确定;
3.4此时可以编写java脚本,记住import类,和eclipse的写法一致就能找到类;
3.5运行脚本,看能否运行通过;
某些配置的具体操作方式:
1、VUGen 工具--》录制选项--》HTTP属性 --》高级 (设置字符集)
2、脚本设置:
2.1、VUGen 工具--》录制选项--》常规--》录制 (默认web使用html)
3、运行时的设置:
VUGen VUSER-->运行时设置--》运行逻辑 (设置运行的迭代次数)
VUGen VUSER-->运行时设置--》常规--》思考时间--》忽略思考时间 (将录制操作过程中的停顿忽略)
4、如果需要操作到达某个点时需要等待其他用户都到达这个点再同时向下执行,在整理代码时,可以在这个点上执行“插入--》集合”;
5、当需要请求的用户来自不同IP时,需要设置Controller的“场景”--》“启用IP欺骗器”;
分层是复杂软件系统常见的设计思路。比如
互联网的七层/五层模型,
Android系统的APP/FWK/JNI/Kernel等,都是通过分层、解耦,达到简化问题,易于维护,便于扩展的效果。
传统的
黑盒测试主要关注客户需求,
白盒测试比较灵活,但实际应用中以验证编码实现为主,两者都忽略了设计这个开发过程中承上启下的环节。
分层测试的核心思想是:针对有明确分层设计的软件系统,采用白盒测试的技术,在层与层之间验证接口的正确性。分层测试以调用接口驱动被测系统,尽量不依赖于打桩(具体原因后面会提到)。去年下半年开始我们在Android测试中尝试分层测试,取得了很好的效果。
1、精准。我们都知道,离问题产生的地方越近,就越容易触发问题。如果问题发生在底层,以白盒测试的方法,很难精确打击,特别是一些复杂场景或异常流程,可能无法构造。而分层测试的切入点就是层与层之间的接口,从机制上更接近出问题的地方,因此也更容易命中目标。
2、低成本。这个优势源于可测试性。举例来说:我们要测试Android系统下拨号的性能,黑盒怎么测呢?测试人员需要打开秒表,同时进行拨号的操作,并观测电话是否拨通。操作麻烦不说,误差也很大。如果用分层测试的方式,只要提供拨号和检查是否拨通两个对外开放的接口,通过用例脚本调用,并记录两者的时间,就可以方便准确地得到耗时。更进一步,我们还可以在不同层次的接口调用时均记录下时间,在脚本中直接对各个环节的耗时进行分析,从而自动分析流程的瓶颈,找到影响性能的关键环节。
再回过头来看前面提到的尽量避免打桩的建议,也是考虑到成本。打桩是白盒测试最困难的部分,特别是涉及到复杂的数据类型或者系统内部状态。因此很多开发同事不愿意使用UT。分层测试重驱动弱打桩,测试脚本主要还是运行在真实的测试环境中,这样就避免了打桩上的投入,也更接近开发的调试手段。
3、高效。这里是指用例执行速度快。首先
自动化测试的速度就明显优于手工测试,基于API调用的自动化又比UI自动化要快,分层测试的高效就建立在API调用高效的基础上。从我们收集的数据来看,相同的用例,手工执行的耗时平均在5-8分钟,UI自动化一般也需要1-2分钟,而分层测试通常10-20秒就完成了,效率提升达10倍。
4、易定位。易定位其实是和精准对应的。在
用例设计的时候就考虑到用例所针对的代码,一旦出现问题,自然就容易定位了。
5、稳定。客户需求是易变的,内部实现也是易变的,但是层与层之间的接口是不同开发人员之间的约定,通常会尽量保持稳定。这里也有一组数据:从Android 4.2到Android 4.4,我们设计的JNI层用例变更不到10%,而针对APP界面开发的用例,变更率高达40%。
6、尽早测试。尽早测试是
敏捷所提倡的,目的是把问题拦截在前端,降低问题修复成本。由于分层测试不依赖于完整系统,可以通过直接调用底层接口进行测试,就不需要等到整个系统开发完成。其实分层测试的思想和自底向上的系统开发模式也是不谋而合的。
介绍了这么多分层测试的优势,那么它是万能的银弹吗?首先,分层测试不是端到端的测试,接口之上的部分无法覆盖,因此无法替代验收测试。另外,分层测试依赖于被测系统良好的分层设计,如果被测系统的结构不清晰,耦合严重,分层测试就不合适了。
本文首先列举了 Dojo 应用 UI
自动化测试所面临的挑战,进而引出设计 Dojo 应用 UI 自动化测试的框架时应考虑的一些原则。对于正从事 Web UI 自动化测试
工作的读者(即便所测试的应用不是 Dojo 应用)或者对这方面感兴趣的读者,本文都有一定的参考价值。
随着富 Internet 应用(RIA)的不断兴起,各种 JavaScript 开发工具包的功能也在不断增强,Dojo 正是其中的佼佼者。Dojo 提供了一套完整的开发解决方案,包括核心的 JavaScript 库、简单易用的小部件(Widget)等。当越来越多的企业 Web 应用选择 Dojo 作为前端技术框架的时候,作为测试人员,我们便需要考虑如何有效地进行自动化测试开发与实施。Dojo 是一个 Web 前端技术框架,所以这里所说的自动化测试,具体是指 Web UI 自动化测试,更进一步说是 Web 2.0 UI 自动化测试。
考虑到 Web UI 的特性以及 Web UI 自动化测试的投入产出比(ROI),关于是否应该进行 Web UI 自动化测试,测试领域存在不同的观点。我个人认为还是有必要的。不过这不是本文要讨论的重点。决定 Web UI 自动化测试成败(或者说收效)的因素有很多,技术框架的选取和设计是其中之一,个人认为也是比较重要的一个因素。本文将从 Dojo 应用 UI 自动化测试面临的诸多挑战谈起,进而讨论针对 Dojo 应用 UI 自动化测试的框架适用的设计原则。
虽然本文探讨的主题是 Dojo 应用的 UI 自动化测试,但不少内容也适合其他的 Web 2.0 应用。希望读者朋友或多或少都能从中获益。
Dojo 是什么?
Dojo 是一个 JavaScript 实现的开源 DHTML 工具包。它是在几个项目捐助基础上建立起来的(nWidgets,f(m),Burstlib) 。 Dojo 的最初目标是解决开发 DHTML 应用程序遇到的一些长期存在的历史问题,现在,Dojo 已经成为了开发 RIA 应用程序的利器:
Dojo 让您更容易地为 Web 页面添加动态能力,您也可以在其它支持 JavaScript 的环境中使用 Dojo ;
利用 Dojo 提供的组件,您可以提升 Web 应用程序的可用性和交互能力;
Dojo 很大程度上屏蔽了浏览器之间的差异性,因此,您可以不用担心 Web 页面是否在某些浏览器中可用;
通过 Dojo 提供的工具,您还可以为代码编写命令行式的
单元测试代码。
Dojo 的打包工具可以帮助您优化 JavaScript 代码,并且只生成部署应用程序所需的最小 Dojo 包集合。
Dojo 应用 UI 自动化测试面临的挑战
Dojo 应用 UI 自动化测试面临的挑战可以归为两类:开发阶段的挑战和维护阶段的挑战。
开发阶段的挑战:
异步请求的处理
元素定位
Dojo 复杂性
产品复杂性
维护阶段的挑战:
频繁的 UI 更改
Dojo 升级
下面,我们具体讲解每一种挑战。为了后文讲解设计原则时能方便的对应到这些挑战,我们用英文字母标记每个挑战。
A. 异步请求的处理
在传统的 Web 应用中,用户每点击页面上的超链,浏览器就会向服务器发出一个请求,等待服务器做出响应,然后返回一个完整新网页,但在大多数情况下用户不得不忍受页面闪烁和长时间的等待。为了解决传统的 Web 应用中出现的问题,出现了 Ajax。通过使用 Ajax 页面和应用向服务器请求它们真正需要的东西,也就是页面中需要修改的部分,服务器就提供哪部分,这使得通信量更少,更新更少,用户等待页面刷新更短,用户更加满意。
但是,Ajax 的到来对 Web UI 自动化测试却未必是好消息。Web UI 自动化测试框架的执行方式类似于一个“队列”:用户定义了一系列的页面操作和验证,之后 Web UI 自动测试框架把它们按顺序一一执行。在传统 Web 应用中,由于每次向服务器端发起请求都会导致页面跳转,Web UI 自动化测试框架能很容易地判断出合适该执行下一个动作。 但是,Ajax 的行为是异步的,也就是说,用户的操作跟服务器端的处理现可以是并行的,不存在严格的顺序。起初,这给 Web UI 自动化测试框架的开发者带来了困扰,而且很长一段时间里这个问题都没有被很好的解决。
起初,Web UI 自动化测试开发人员主要有两种解决方案:硬编码等待时间和基于条件的等待。
硬编码等待时间: 这种方式是指在代码中指定一个静态等待时间,比如指定等待 30 秒后执行下一个操作。通常,这种数值的设置来源于经验。可以想象,如果这个等待时间比实际等待时间小,自动化脚本执行就会失败。而且,不幸的是,这样情况的确经常发生。随着测试环境的变化,实际需要的等待时间事实上是一个变量。以下几个因素都有可能导致它发生变化:
测试服务器在远程还是本地:毫无以为,由于网络的影响,服务器如果在远程,等待时间就会增加。
使用的浏览器: 浏览器 JavaScript 引擎的执行速度对等待时间也有影响。根据经验, Firefox 和 Chrome 都比 IE 要快。因此,如果用的是 IE,等待时间也会增加。 测试运行时的网络环境:如果测试时的网速较慢,等待时间会增加。
测试服务器的负载:测试服务器可能是独占也可能是共享的。如果自动化测试执行时的服务器负载较大,也会导致等待时间增加。
对于上述硬编码等待时间的问题,Web UI 自动化测试开发人员一般有如下对策。
尽量将等待时间设置的大一些。这样做存在一些问题。一来未必能保证在任何情况下该值都足够大;二来它意味着无论服务器在本地还是远程浏览器是快是慢,执行时间都一样,这对于服务器在本地以及浏览器比较快的测试环境是很大的时间成本浪费。
设置放大因子。这种方法是在编码的时候给等待时间乘上一个因子。这个因子或者是硬编码的,比如定义在一个参数文件中。当测试环境比较好的时候,设置小一点;测试环境比较差的时候,设置大一点。 高级一些的,可以动态地设置这个因子。比如,可以把某一个操作的执行时间作为计算该因子数值的参照。这个方法使情况得到了部分改善,但是仍然不能保持其执行的稳定性。
基于条件的等待: 由于硬编码等待时间方式的不稳定性,Web UI 自动化测试框架开始支持“基于条件的等待”。这种方式是采用“轮询”的机制判断是否可以开始执行一个操作。假设,当前要执行的操作是一个按钮点击动作。这个按钮默认是被禁用的,当页面从服务器端加载了数据之后这个按钮才被启用。因此,如果要保证这个操作成功,就需要“轮询”这个按钮的状态 – 直到发现它被启用了,才执行点击操作。 如果由于存在 bug 按钮一直不启用怎么办?所以,基于条件的等待的方法需要设置“超时时间”。如果超过这个时间按钮还没有启用,就认为这个测试用例失败了。所以,大家发现了,这个超时时间的设置其实还是个“硬编码等待时间”,所以某种程度上,这种方式仍存在硬编码等待时间方式的弊端。这种方式的另外一弊端是它带来了额外的编码成本,并且自动化测试代码中可能充斥了许多条件判断代码,而它们本身不完成我们的“业务”目标,仅是辅助代码。
B. 元素定位
一般的 Web UI 自动化测试编码无非包含这样几个步骤:定位及获取元素,执行操作以及验证结果。而且,通常 Web UI 自动化测试开发人员在元素定位和获取这个步骤上花费相当多的时间。大多数 Web UI 自动化测试开发人员希望所需获得的元素能有一个静态 id 属性。根据规范,id 属性的值在 DOM 树中必须是唯一的,因此通过 id 属性就可以很简单并且准确地定位元素。但是,这样的需求常常不是产品的需求,而只是为了满足 Web UI 自动化测试代码开发的简便。所以,对于产品开发人员来说,给每个需要在 Web UI 自动化测试中使用到的元素增加静态 id 属性不是高优先级的工作。而且,从技术角度上讲,有些 Dojo 应用中的元素也无法设置静态 id 属性。
对于 Dojo 应用,情况又复杂了一些。图 1 展示的是一个 Dojo 按钮小部件在 Firebug 中呈现的 DOM 结构。最外层的 SPAN 元素上有一个 widgetid 属性,内部的 SPAN 元素上定义了 id 属性,共同点是它们都是由一个前缀和一个动态的数字构成的。这个数字是由运行时该 Dojo 小部件在整个 DOM 树上的位置决定的,索引从 0 开始。因此可知,图 1 中的按钮小部件是整个 DOM 树上的第二个按钮。Widgetid 是每个 Dojo 小部件都有的属性,如没有特别指定的话,它就是如图中所示的动态结构。
所以,如何通过这种动态格式的 id 和 widgetid 定位获取页面中的元素成为另一个 Dojo 应用 UI 自动化测试开发的挑战。
图 1. Dojo 按钮小部件
C. Dojo 复杂性
读者不要误会,这里说的“Dojo 复杂性”不是说 Dojo 使用起来很复杂,而是说 Dojo 应用最后生成的页面 DOM 结构比较复杂。如图 1 所示,原本用一个 INPUT 元素实现的按钮,现在嵌套了四层 SPAN 元素。 所以,Dojo 小部件已不再是一个简单的 HTML 元素,而是由诸多 HTML 元素组合而成的。Dojo 按钮小部件只是一个简单的例子,对于像表格、日历之类的 Dojo 小部件,它们的结构更加复杂。针对某个 Dojo 小部件的操作也不再像操作普通的 HMTL 元素那样简单(调用某个 JavaScript 方法就可以实现),而是要具体定位响应事件(比如点击事件)的 Dojo 小部件的内部对应元素,然后触发具体的事件。
多数的 Web UI 自动化测试框架只能处理基本的 HTML 元素。这是合理的,因为框架开发者不知道最终的自动化测试开发者使用什么样的前段框架,因而只能提供最基本的“小砖块”。如果具体应用的自动化测试开发者有任何需求,可以在其之上进行二次开发。
对于 Dojo 应用的 UI 自动化测试开发者来说,可以选择直接定位和操作 Dojo 小部件内部元素的方法。但是,这样做的弊端很明显 – 缺乏重用性。 因此,应该将 Dojo 小部件作为最小操作单元。如何将 Dojo 小部件模块化成为了又一个挑战。
D. 产品复杂性
产品复杂性包含两层意思。
一是指产品本身页面层次多,界面元素多以及功能逻辑复杂。Web UI 自动化测试开发过程中,编写定位元素的代码通常占据很大一部分时间,编写这部分代码的工作量直接受页面元素多少的影响。功能复杂度一般不太会直线性增加编码工作量,但是有可能会增加调试的工作量。
二是指产品页面 DOM 结构复杂。Web 2.0 应用的开发模式与传统 Web 应用的开发模式有很大的不同。传统 Web 应用开发基本上是一个页面对应一个物理的程序文件(比如,.jsp 文件)。而 Web 2.0 应用的开发通常是整个应用只有少数几个物理的程序文件,页面的跳转和呈现通过 JavaScript 更新 DOM 树结构完成。所以,在浏览器中的跳转页面实际上往往只是 DOM 树结构被更新了。这样,就有可能带来这样一个问题:DOM 树上同时存在多个页面的 DOM 子节点。如果其他页面有与当前要定位的元素类似的元素,就很容易出现“错误定位”。
如何能在产品变得越来越复杂的同时,尽可能的减少开发工作量,也是一个 Dojo 应用 UI 自动化测试开发者要解决的问题。
E. 频繁的 UI 更改
频繁的 UI 更改在“敏捷开发”大行其道之后成为了又一个让 Web UI 自动化测试开发者头痛的问题。迭代中的产品 UI 经常随着客户的反馈不断地修改,于是自动化测试代码需要跟着更新。由此带来的 Web UI 自动化测试脚本巨大的维护工作量也成为了很多人反对进行 Web UI 自动化测试的重要原因之一。
难道,因此就放弃 Web UI 自动化测试吗?我认为,作为 Web UI 自动化测试开发者需要考虑的应该是:在这样的现实之下,如何能够最小化频繁的 UI 更改带来的代码维护工作量。从经验来看,并非 Web UI 上的任何一点点更改都会导致自动化测试代码的修改。这个问题的解决极大程度上依赖于测试框架适应 DOM 结构变化的能力以及元素定位时所使用的方法。
F. Dojo 升级
Dojo 升级有可能会引发 Dojo 小部件 DOM 结构的变化,继而带来自动化测试代码的维护工作量增加。尽管根据经验,这一点的影响不是很显著,因为目前一些常见的 Dojo 小部件(比如按钮输入框之类)都已经十分成熟,但是还是需要考虑。如果一个小部件的 DOM 结构发生了变化,如何能够最小化对自动化测试代码的影响?这是 Dojo 应用 UI 自动化测试开发者要考虑的问题。
Dojo 应用 UI 自动化测试框架挑选(设计)原则
讲解完了所有的挑战,让我们来看看当我们要设计或者选取一个 Dojo 应用 UI 自动化测试框架时需要考虑的因素和原则。
概括起来,主要有以下八大原则。括号内是该原则所对应解决的挑战。
隐式等待(A)
位置关系操作(B,E)
封装 Dojo 小部件 (C,D,F)
IBM 框架 (E)
代码生成(D)
测试数据的管理(D)
录制与编码 (D)
接下来,我们一一来讲解每一条原则。为了帮助读者理解,讲解过程中会引用内部自行开发的 Dojo 应用 UI 自动化测试框架 Water Bear 的部分内容。
隐式等待
挑战 A 提到了异步请求处理的问题也描述了几种解决方案,但都不甚理想。最理想的情况是自动化测试框架能够自己处理异步请求的问题,并且不需要开发者编写任何代码。这就是所谓的“隐式等待”机制。
目前,不少 Web UI 自动化测试框架都具备“隐式等待”功能,比如 Sahi 和 Selenium。Water Bear 使用的是 Sahi。当然,Sahi 同时也提供了对“硬编码等待时间”和“基于条件的等待”的 API 支持(因为,有些情况下还是不得不使用“硬编码等待时间”或“基于条件的等待”)。根据经验,超过 90%的情形是 Sahi 的“隐式等待”功能可以覆盖的,但是像“进度条”这样的功能就不得不用“基于条件的等待”。在进度条达到 100%之前,前端代码不断地向后台发起 Ajax 请求轮询进度状态。这种情形,Sahi 无法智能地得知哪一个 Ajax 请求的返回是“最后一个”,因此,就无法决定何时可以执行下一个动作。
清单 1. “进度条”代码示例
BrowserCondition cond = new BrowserCondition(getBrowser()) {
public boolean test() throws ExecutionException {
return getBrowser().isVisible(getBrowser().div("100%").in(es));
}
};
getBrowser().waitFor(cond, waitTime);
清单 1 展示了 Water Bear 中部分实现进度条功能的代码,也就是 Sahi“基于条件的等待”的代码。在 test 方法返回“真”之前(也就是代码发现文本“100%”出现了),waitFor 方法一直处于轮询状态。waitTime 是一个超时时间。如果这个时间达到了,即便 test 方法仍然返回“假”,waitFor 还是返回。通常,这时已经可以认定 Web UI 的功能执行出现了问题。
只所以把“隐式等待”放在第一个来讲,是因为 Web UI 自动化测试框架是否具备该功能对于 Dojo 应用 UI 自动化测试实施成败是至关重要的。
位置关系操作
Dojo 应用 UI 自动化测试框架应当具备根据给定对象定位其他对象的功能。这种相对的位置关系通常包括:在里面、在附近、在左面(右面)以及在上面(下面)等等。目前,Sahi 支持所有这些位置关系查找,另外还支持“在上面或者下面”以及“在左面或者右面”。这当中,“在里面”使用的最为频繁。
为什么要使用位置关系操作?有以下几个原因:
准确定位页面对象。在挑战 D 中,我们提到了在一个 DOM 树中有可能存在与要查找的对象属性与 DOM 结构完全相同的对象。尤其对于“确定”或者“取消”按钮,这种情况很常见。这时,必须要通过限定父对象来缩小查找范围。一般来讲,只要限制到当前页面的边界元素能够阻止跨页面 DOM 节点查找就够了(因为,当前页面有哪些对象一目了然,但是其他页面的 DOM 节点是由运行时的操作控制的,无法预料)。
封装 Dojo 小部件。这一点会在下面具体讲解。
提供更丰富的查找方式,如根据“标签”查找 Dojo 小部件。通常,文本框的左侧会有一个文本,比如“名称:”。如果,该文本框没有任何其他合适的静态属性用来定位,就可以通过“在附近”查找的方法,先定位“名称:”所在的元素,再找到这个文本框。
当 DOM 树结构未发生“功能性”变化时,避免自动化测试代码改动。所谓“功能性”变化是指页面上增加、替换或者移除了控件,比如按钮或者输入框等。当发生这些变化时,自动化测试代码不得不修改。但,如果发生的仅仅是 DOM 树结构上的变化,比如增加了不可见的 DIV 或者 SPAN 元素,只要位置关系操作使用得当,自动化测试代码可以做到不需要修改。
我们来看一个例子。图 2 左边是所测应用现在的状态:一个 Dojo 按钮小部件被一个 DIV 元素包围。在代码中,我们用按钮的文本“OK”并限制在图示的 DIV 中来定位它。之后,DOM 树结构发生了变化,变成了图 2 右边的样子,在原有的 DIV 元素和按钮之间增加了两层 DIV 元素。此时,代码不需要更改,因为这个“OK”按钮仍然在之前的 DIV 元素里。但是,如果用 XPath 定位元素,这种情况就很有可能要修改了。
图 2. 位置关系操作示例
封装 Dojo 小部件
在挑战 C 中,提到了 Dojo 小部件 DOM 结构的复杂性。最好的方式是封装 Dojo 小部件,将每个 Dojo 小部件作为一个整体操作。
Water Bear 使用的编程语言是 Java。它将页面中的 Dojo 小部件映射成一个 Java 类,以此封装所有该 Dojo 小部件的行为。由于 Sahi 支持“在里面”的位置关系操作,使得封装更加容易 – 只需要定位到 Dojo 小部件的最外层 HTMl 元素(一般是 DIV),其他的元素定位都限制在该元素之内。从而,即便页面上同时有两个同类型的 Dojo 小部件,也不会出现跨部件的错误操作。
图 3 展示了 Water Bear 中定义的与 Dojo 小部件对应的一些类。最上层父类 WebElement.java 代表一般的 HTML 元素,它封装了基本 HTML 元素的方法。DojoWidget.java 是所有 Dojo 小部件类的抽象。它定义了所有 Dojo 小部件类的通用方法,比如 getWidgetId()等。所有 Dojo 小部件类均继承自 DojoWidget.java 并定义自己的方法,比如 ComboBox.java 需要定义 selectByText 方法。
图 3. Water Bear 中与 Dojo 小部件对应的类
那么,页面上 Dojo 小部件如何和这些类对应关联起来呢?Water Bear 中通过.wdef 文件把两者关联起来。
清单 2. .wdef 文件示例
org.waterbear.core.widgets.dijit.Textbox=div|textbox|labelnear|dijit_form_ValidationTextBox,\
evo_form_ObjectNameTextBox,dijit_form_NumberTextBox,dijit_form_TextBox,aspen_config_directory_DNTextBox
org.waterbear.core.widgets.dijit.Checkbox=div|checkbox|labelnear|dijit_form_CheckBox
org.waterbear.core.widgets.dijit.Select=table|table|labelnear|dijit_form_Select
org.waterbear.core.widgets.dijit.Button=span|span|labelinside|dijit_form_Button
org.waterbear.core.widgets.dijit.ComboBox=div|textbox|labelnear|dijit_form_ComboBox
清单 2 展示了.wdef 文件的部分内容。它是一纯文本文件,每一行定义了一组映射关系。等号左边是 Dojo 小部件映射的 Java 类名,等号右边是页面上 Dojo 小部件的属性描述。其中,起关键作用的是是最后一个属性,它是 widgetid 的前缀部分。一个 Dojo 小部件类可以关联到多种页面 Dojo 小部件,因为有些 Dojo 小部件虽然被扩展了,但是对 Web UI 自动化测试来说扩展的功能不会被涉及到,于是可以用同一个类表示。
IBM 框架
IBM 框架以前被称作为 ITCL 框架,由质量软件工程(Quality Software Engineering) 和 IBM 中有经验的自动化团队合作开发而成的。这个框架由三层架构组成,架构的实现贯穿了应用对象、任务和测试用例包(IBM 包)。
潜在于应用对象、任务和测试用例包之下的基本原理是:
层次化的体系架构
将“做什么”与“如何做”分离开来
代码重用
一致和清晰的组织结构
快速增强的能力
迅速的调试
有效地组织文件
启用协作
学习他人
下面是对应用对象、任务和测试用例的解释说明:
应用对象:储存有关你的应用程序中的 GUI 元素信息。同时在这里也可以编写你的 Getter 方法,这些 Getter 方法可以返回对象,使调用者能够对这些 GUI 元素进行查询和操作。一般情况下,这些方法在 Task 层中进行调用。
任务:在这里你将编写可重用的方法,这些方法在你的应用程序中执行通用功能。同时在这里,你将编写可以处理和查询复杂的特定应用程序控件的方法。在任务中的方法可以被测试用例调用。
测试用例:导航一个应用程序,验证其状态,并记录其结果的方法。
这个理念应用到 Java 编程就是用不同“包”组织应用对象、任务和测试用例的类。无论具体的底层 Web UI 自动化测试框架是什么,用 IBM 框架的理念来架构上层测试用例的类都是有帮助的。 采用 IBM 框架能够有效地应对挑战 E,因为高重用性能够将代码改动控制在局部范围,并且一处改动即可纠正所有引用的代码。除了采用 IBM 框架,实际上如何合理地规划任务方法的粒度也很关键。但,这与具体应用就十分相关了。
代码生成
一般来说,编写用来获取界面元素的代码(也就是应用对象层的代码)在整个开发工作量中占据很大的比例。通常,它需要开发者先借助一些能够显示 DOM 树结构的工具(比如 Firebug)观察元素的属性,然后决定用什么属性来定位元素比较合适。 另外,一些代码又是比较“机械”或者模式化的,例如给界面元素赋值的操作。这样的代码应尽可能的通过一些自动化的方式生成,就可大大节省开发的时间。
在 Water Bear 中,针对以上的两个问题,分别开发了两种代码生成工具。
一个工具叫 AppObjs CodeGen,它可以自动遍历当前的 DOM 树,从而生成应用对象层的代码。它用 Sahi 脚本实现,将结果输出在它的日志窗口中(如图 4)。 开发者将代码复制到自己的类文件中即可。尽管这些代码仍可能需要一些手工修改,比如重命名以增强方法的可读性,但较之完全用手工编码已节省很多时间。
图 4. AppObjs CodeGen 生成结果
第二个工具叫 Skeleton CodeGen。 它的输入是应用对象层的类,输出是任务和测试用例层的部分代码。对于符合特定模式的以及界面有很多 UI 控件的 Dojo 应用,这个工具比较有效。生成代码仍需要二次修改,因为有些逻辑无法自动生成。
测试数据的管理
最好能够将测试数据和测试用例代码分离,这样同样一份测试用例代码可以搭配 N 种不同的测试数据运行。另外,这种分离也可以轻松地做到为不同的测试环境定义不同的测试数据 – 有时候,一些测试数据是有环境依赖的。
Water Bear 利用开源软件 XStream 来实现测试数据与测试用例代码的分离。首先,测试数据被定义在一个 HashMap 中。测试用例在第一次运行时会将 HashMap 通过 XStream 自动序列化成一个.xml 文件。当自动化脚本再次运行时,如果.xml 文件已经存在,它就直接读取已有文件并借助 XStream 把文件内容反序列化成 HashMap。在之后的运行中,只要 HashMap 的数据结构没有变化,就无需重新生成.xml 文件。数据文件的存放目录可以通过配置文件指定,因此对于不同的环境只要修改配置文件指向不同的路径即可。
录制与编码
提高开发效率的另一个途径是提供录制功能。但是录制功能通常有如下一些问题:
录制生成的代码通常是“过程化”的。“过程化”的代码很难重用。即便是针对同一个页面功能模块录制的不同的测试用例代码也是孤立的。如果页面功能发生变化,所有测试用例需要重新录制。因此,表面上看录制功能似乎大大减少了开发时间,但是当页面发生变化的时候,它或许未必如我们想象的那么好。
录制的代码可读性通常比较差。一般来说,录制之后仍需要重命名变量以提高代码可读性。
所以,理想中的录制功能生成的代码应该是模块化的,也就是按照应用对象、任务和测试用例这三个层次生成。这样的代码即便于二次修改,也便于与已有代码集成。从使用的角度看,所有的测试用例代码完全用录制功能生成不太现实,较好的方式是只用录制功能辅助开发应用对象层和任务层代码。 当 UI 发生非功能性变化时,只需要重新录制受影响的任务方法并更新应用对象层。理论上,测试用例层不需要修改(除非测试用例的逻辑不得不修改)。
Water Bear 目前仅支持“过程化”录制功能。图 5 展示的就是 Water Bear Recorder 生成的代码。开发者把生成的代码复制黏贴到自己的测试类中即可在 Water Bear 中直接运行。
图 5. Water Bear Recorder
结束语
本文首先列举了 Dojo 应用 UI 自动化测试面临的诸多挑战,之后从这些挑战入手,结合个人的开发经验以及一个内部使用的 Dojo 应用 UI 自动化测试框架,提出了八个在选取和设计 Dojo 应用 UI 自动化测试框架时需要考虑的原则。当然,每一点的重要性不尽相同。比如“隐式等待”是必须的,而录制功能却没有那么重要。但是,若是这些原则都能符合,将大大提高 Dojo 应用 UI 自动化测试实施成功的可能性。
在
Linux系统下,重启Tomcat使用命令操作的!
首先,进入Tomcat下的bin目录
cd /usr/local/tomcat/bin
使用Tomcat关闭命令
./shutdown.sh
查看Tomcat是否以关闭
如果显示以下相似信息,说明Tomcat还没有关闭
root 1971 1 0 Apr19 ? 00:00:04 /opt/jdk1.6.0_37/bin/java -Djava.util.logging.config.file=/opt/tomcat8080/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djava.endorsed.dirs=/opt/tomcat8080/endorsed -classpath /opt/tomcat8080/bin/bootstrap.jar -Dcatalina.base=/opt/tomcat8080 -Dcatalina.home=/opt/tomcat8080 -Djava.io.tmpdir=/opt/tomcat8080/temp org.apache.catalina.startup.Bootstrap start root 6016 5899 0 09:14 pts/1 00:00:00 grep java |
*如果你想直接干掉Tomcat,你可以使用kill命令,直接杀死Tomcat进程
kill -9 1971
然后继续查看Tomcat是否关闭
ps -ef|grep java
如果出现以下信息,则表示Tomcat已经关闭
root 6019 5899 0 09:15 pts/1 00:00:00 grep java
最后,启动Tomcat
./startup.sh
注意:使用root用户登录Linux系统;正确进入Tomcat目录;在确定Tomcat关闭之后再启动Tomcat,否则会报端口被占用异常。
附图:
在做
软件开发时,如果选用
MySQL数据库的系统,当需要存储数据量大或数据操作很频繁时,MySQL对于性能的影响很大,也是关键的核心部分。MySQL的设置是否合理优化,直接影响到软件运行的速度和承载量。同时,MySQL也是优化难度最大的一个部分,不但需要理解一些MySQL专业知识,同时还需要长时间的观察统计并且根据经验进行判断,然后设置合理的参数。下面我们研究一下MySQL(my.cnf/my.ini)的配置文件,通过在(my.cnf/my.ini)中添加/修改参数项来对Mysql进行优化。
linux环境MySql配置优化
1、配置文件
MySQL服务器的许多参数会影响服务器的性能表现,而且我们可以把这些参数保存到配置文件,使得每次MySQL服务器启动时这些参数都自动发挥作用。这个配置文件就是my.cnf。
MySQL服务器提供了my.cnf文件的几个示例,它们可以在/usr/local/mysql/share/mysql/目录下找到,名字分别为 my-small.cnf、my-medium.cnf、my-large.cnf以及my-huge.cnf。文件名字中关于规模的说明描述了该配置文件适用的系统类型。例如,如果运行MySQL服务器的系统内存不多,而且MySQL只是偶尔使用,那么使用my-small.cnf配置文件最为理想,这个配置文件告诉mysqld daemon使用最少的系统资源。反之,如果MySQL服务器用于支持一个大规模的在线网站,系统拥有4G以上的内存,那么使用mysql-huge.cnf 最为合适。
要使用上述示例配置文件,我们应该先复制一个最适合要求的配置文件,并把它命名为my.cnf。这个复制得到的配置文件可以按照如下三种方式使用:
全局:把这个my.cnf文件复制到服务器的/etc目录,此时文件中所定义的参数将全局有效,即对该服务器上运行的所有MySQL数据库服务器都有效。
局部:把这个my.cnf文件复制到[MYSQL-INSTALL-DIR]/var/将使该文件只对指定的服务器有效,其中[MYSQL-INSTALL-DIR]表示安装MySQL的目录。
用户:最后,我们还可以把该文件的作用范围局限到指定的用户,这只需把my.cnf文件复制到用户的根目录即可。
那么,如何设置my.cnf文件中的参数呢?或者进一步说,哪些参数是我们可以设置的呢?所有这些参数都对MySQL服务器有着全局性的影响,但同时每一个参数都和MySQL的特定部分关系较为密切。例如,max_connections参数属于mysqld一类。那么,如何才能得知这一点呢?这只需执行如下命令:
%>/usr/local/mysql/libexec/mysqld –help
该命令将显示出和mysqld有关的各种选项和参数。要寻找这些参数非常方便,因为这些参数都在“Possible variables for option –set-variable (-O) are”这行内容的后面。找到这些参数之后,我们就可以在my.cnf文件中按照如下方式设置所有这些参数:
例如:
set-variable = max_connections=1000
这行代码的效果是:同时连接MySQL服务器的最大连接数量限制为1000。
2、参数优化
2.1 back_log
back_log = 500要求 MySQL 能有的连接数量。当主要MySQL线程在一个很短时间内得到非常多的连接请求,这就起作用,然后主线程花些时间(尽管很短)检查连接并且启动一个新线程。
back_log值指出在MySQL暂时停止回答新请求之前的短时间内多少个请求可以被存在堆栈中。只有如果期望在一个短时间内有很多连接,你需要增加它,换句话说,这值对到来的TCP/IP连接的侦听队列的大小。你的
操作系统在这个队列大小上有它自己的限制。试图设定back_log高于你的操作系统的限制将是无效的。当你观察你的主机进程列表,发现大量 264084 | unauthenticated user | xxx.xxx.xxx.xxx | NULL | Connect | NULL | login | NULL 的待连接进程时,就要加大 back_log 的值了。默认数值是50,如果访问量大可以它改为500。
2.2 key_buffer_size
Key_read_requests :从缓存读取索引的请求次数;
Key_reads :从磁盘读取索引的请求次数;
key_buffer_size指定用于索引的缓冲区大小。如果Key_reads太大,则应该把key_buffer_size变大。增加它可得到更好处理的索引(对所有读和多重写),如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。对于内存在4GB左右的服务器该参数可设置为384M或512M。通过检查状态值Key_read_requests和 Key_reads,可以知道key_buffer_size设置是否合理。比例key_reads / key_read_requests应该尽可能的低,至少是1:100,1:1000更好(上述状态值可以使用SHOW STATUS LIKE ‘key_read%'获得)。注意:该参数值设置的过大反而导致是服务器整体效率降低。
2.3 max_allowed_packet
设置最大包,限制server接受的数据包大小,避免超长SQL的执行有问题 默认值为16M,当MySQL客户端或mysqld服务器收到大于max_allowed_packet字节的信息包时,将发出“信息包过大”错误,并关闭连接。对于某些客户端,如果通信信息包过大,在执行查询期间,可能会遇到“丢失与MySQL服务器的连接”错误。
增加该变量的值十分安全,这是因为仅当需要时才会分配额外内存。例如,仅当你发出长查询或mysqld必须返回大的结果行时mysqld才会分配更多内存。该变量之所以取较小默认值是一种预防措施,以捕获客户端和服务器之间的错误信息包,并确保不会因偶然使用大的信息包而导致内存溢出。
2.4 table_cache
table_cache = 512指定表高速缓存的大小, 如果opened_tables太大,应该把table_cache变大。每当MySQL访问一个表时,如果在表缓冲区中还有空间,该表就被打开并放入其中,这样可以更快地访问表内容。通过检查峰值时间的状态值Open_tables和Opened_tables,可以决定是否需要增加table_cache的值。如果你发现 open_tables等于table_cache,并且opened_tables在不断增长,那么你就需要增加table_cache的值了(上述状态值可以使用SHOW STATUS LIKE ‘Open%tables'获得)。注意,不能盲目地把table_cache设置成很大的值。如果设置得太高,可能会造成文件描述符不足,从而造成性能不稳定或者连接失败。
2.5 sort_buffer_size
属重点优化参数
sort_buffer_size = 4M查询排序时所能使用的缓冲区大小。注意:该参数对应的分配内存是每连接独占,如果有100个连接,那么实际分配的总共排序缓冲区大小为100 × 4 = 400MB。所以,对于内存在4GB左右的服务器推荐设置为4-8M。
2.6 read_buffer_size
read_buffer_size = 4M读查询操作所能使用的缓冲区大小。和sort_buffer_size一样,该参数对应的分配内存也是每连接独享。MySql读入缓冲区大小。对表进行顺序扫描的请求将分配一个读入缓冲区,MySql会为它分配一段内存缓冲区。read_buffer_size变量控制这一缓冲区的大小。如果对表的顺序扫描请求非常频繁,并且你认为频繁扫描进行得太慢,可以通过增加该变量值以及内存缓冲区大小提高其性能。
2.7 join_buffer_size
join_buffer_size = 8M联合查询操作所能使用的缓冲区大小,和sort_buffer_size一样,该参数对应的分配内存也是每连接独享。
2.8 thread_cache_size
thread_cache_size = 64服务器线程缓存这个值表示可以重新利用保存在缓存中线程的数量,如果Threads_created太大,就要增加thread_cache_size的值。当断开连接时如果缓存中还有空间,那么客户端的线程将被放到缓存中,如果线程重新被请求,那么请求将从缓存中读取,如果缓存中是空的或者是新的请求,那么这个线程将被重新创建,如果有很多新的线程,增加这个值可以改善系统性能.通过比较 Connections 和 Threads_created 状态的变量,可以看到这个变量的作用
2.9 max_connections
max_connections = 300指定MySQL允许的最大连接进程数。如果在访问论坛时经常出现Too Many Connections的错误提示,则需要增大该参数值。
2.10 max_connect_errors
max_connect_errors = 10000000对于同一主机,如果有超出该参数值个数的中断错误连接,则该主机将被禁止连接。如需对该主机进行解禁,执行:FLUSH HOST;。
2.11 wait_timeout
wait_timeout = 10指定一个请求的最大连接时间,对于4GB左右内存的服务器可以设置为5-10。
2.12 thread_concurrency
属重点优化参数
thread_concurrency = 8设置thread_concurrency的值的正确与否, 对mysql的性能影响很大, 在多个cpu(或多核)的情况下,错误设置了thread_concurrency的值, 会导致mysql不能充分利用多cpu(或多核), 出现同一时刻只能一个cpu(或核)在工作的情况。thread_concurrency应设为CPU核数的2倍. 比如有一个双核的CPU, 那么thread_concurrency的应该为4; 2个双核的cpu, thread_concurrency的值应为8。
2.13 tmp_table_size
如果Created_tmp_disk_tables太大, 就要增加tmp_table_size的值,用基于内存的临时表代替基于磁盘的。tmp_table_size 的默认大小是 32M。如果一张临时表超出该大小,MySQL产生一个 The table tbl_name is full 形式的错误,如果你做很多高级 GROUP BY 查询,可以增加 tmp_table_size 值。
2.14 read_rnd_buffer_size
MySql的随机读(查询操作)缓冲区大小。当按任意顺序读取行时(例如,按照排序顺序),将分配一个随机读缓存区。进行排序查询时,MySql会首先扫描一遍该缓冲,以避免磁盘搜索,提高查询速度,如果需要排序大量数据,可适当调高该值。但MySql会为每个客户连接发放该缓冲空间,所以应尽量适当设置该值,以避免内存开销过大。
2.15 query_cache_size属重点优化参数
这个参数也是一个重要的优化参数。但随着发展,这个参数也爆露出来一些问题。机器的内存越来越大,人们也都习惯性的把以前有用的参数分配的值越来越大。这个参数加大后也引发了一系列问题。我们首先分析一下 query_cache_size的工作原理:一个SELECT查询在DB中工作后,DB会把该语句缓存下来,当同样的一个SQL再次来到DB里调用时,DB在该表没发生变化的情况下把结果从缓存中返回给Client。这里有一个关建点,就是DB在利用Query_cache工作时,要求该语句涉及的表在这段时间内没有发生变更。那如果该表在发生变更时,Query_cache里的数据又怎么处理呢?首先要把Query_cache和该表相关的语句全部置为失效,然后在写入更新。那么如果Query_cache非常大,该表的查询结构又比较多,查询语句失效也慢,一个更新或是Insert就会很慢,这样看到的就是Update或是Insert怎么这么慢了。所以在数据库写入量或是更新量也比较大的系统,该参数不适合分配过大。而且在高并发,写入量大的系统,建系把该功能禁掉。
数组就是相同数据类型的元素按一定顺序排列的集合,就是把有限个类型相同的变量用一个名字命名,然后用编号区分他们的变量的集合,这个名字成为数组名,编号成为下标。组成数组的各个变量成为数组的分量,也称为数组的元素,有时也称为下标变量。数组是在程序设计中,为了处理方便, 把具有相同类型的若干变量按有序的形式组织起来的一种形式。这些按序排列的同类数据元素的集合称为数组。 ——这是
百度对数组的定义
首先,数组的定义:
元素类型[] 数组名 = new 元素类型[] {元素1,元素2,........} 例如int[] arr = new int[5]
public static void main(String[] args) { //定义一个可以存放3个数组的容器,注意,数组的脚标从0开始 int[] arr = new int[3]; //位数组赋值 arr[0] = 1; arr[1] = 2; arr[2] = 3; System.out.println(arr[0]); } 也可以这样写 public static void main(String[] args) { int arr[] = {1,2,3}; System.out.println(arr[0]); } |
后者直接体现了数组的值和个数。数据明确的情况下可以前面中,不明确时用后者。
public static void main(String[] args) {
int arr[] = {1,2,3};
arr = null;
System.out.println(arr[1]);
}
用数组的常见错误:
public static void main(String[] args) { int arr[] = {1,2,3}; System.out.println(arr[3]); } //ArrayIndexOutOfBoundsException:数组越界;访问数组时,访问到数组中不存在的脚标。 public static void main(String[] args) { int arr[] = {1,2,3}; arr = null; System.out.println(arr[1]); } //NullPointerException:空指针;当引用没有任何指向值为null的情况,该引用还在用于操作实体。 |
数组的操作
获取数组中德数据,通常会用到遍历
用for循环遍历数组
public static void main(String[] args) { int arr[] = { 1, 2, 3, 9, 5, 8, 6, 32, 52, 100 }; //arr.length方法获取数组的长度 for (int i = 0; i < arr.length; i++) { System.err.println(arr[i]); } } |
java是一门解释执行的语言,由开发人员编写好的java源文件先编译成字节码文件.class形式,然后由java虚拟机(JVM)解释执行,.class字节码文件本身是平台无关的,但是jvm却不是,为了实现所谓的一次编译,到处执行,sun提供了各个平台的JVM实现--也就是说jvm不是跨平台的,编译好的字节码文件被放在不同的
操作系统平台上的jvm所解释执行,这个章节主要解释一下JVM装载类的机制
1.ClassLoader是什么?
一个类如果要被JVM所调度执行,必须先把这个类加载到JVM内存里,java.lang下有个很重要的类ClassLoader,这个类主要就是用来把指定名称(指定路径下)的类加载到JVM中
2.ClassLoader的分类
主要分4类,见下图橙色部分
JVM类加载器:这个模式会加载JAVA_HOME/lib下的jar包
扩展类加载器:会加载JAVA_HOME/lib/ext下的jar包
系统类加载器:这个会去加载指定了classpath参数指定的jar文件
用户自定义类加载器:sun提供的ClassLoader是可以被继承的,允许用户自己实现类加载器
类加载器的加载顺序如图所示:
web_concurrent_start和web_concurrent_end
web_concurrent_start
语法:
int web_concurrent_start ( [char * ConcurrentGroupName,]NULL );
参数:
ConcurrentGroupName:可选的,并发组的标识符。
NULL:参数列表结束的标记符。
返回值:
整型,返回LR_PASS (0)表示成功,返回LR_FAIL(1)表示失败。
web_concurrent_end
语法:
int web_concurrent_end ( reserved );
参数:
reserved:保留的供扩展的字段。
返回值
整型。返回LR_PASS (0)表示成功,返回LR_FAIL(1)表示失败。
说明
web_concurrent_start函数是并发组开始的标记。web_concurrent_end,并发组结束的标记。在并发组中的函数不是立即执行的。在并发组开始时,所有的函数首先被记录下来,当并发组结束时,所有的函数并发执行。脚本执行时,碰到 web_concurrent_end函数时,开始并发执行所有记录的函数。
在并发组中,可以包含的函数有:web_url、web_submit_data、web_custom_request、web_create_html_param、web_create_html_param_ex、web_reg_save_param、web_add_header。
URL-based 方式将每条客户端发出的请求录制成一条语句,对
LoadRunner来说,在该模式下,一条语句只建立一个到服务器的连接,LoadRunner提供了web_concurrent_start和web_concurrent_end函数模拟HTML-based的
工作方式
LoadRunner变量
Loadrunner变量分为局部变量和全局变量。
1. 如何定义变量
在Loadrunner脚本中变量必须在开头声明。比如说,在vuser_init、Action、vuser_end定义变量,必须现在最顶端生命,后面才能使用。如果不这样,编译器是通不过的。
2. 局部变量
就像C语言一样,变量有作用域范围,局部变量包含在函数或者vuser_init、Action、vuser_end内部,对于其他范围之外就不起作用了。
3. 全局变量
若要跨越vuser_init、Action、vuser_end定义变量,则需要定义全局变量。在Loadrunner的脚本录制时,会生成一个globals.h文件,在这里定义的变量相当于Loadrunner脚本的全局变量,可以在vuser_init、Action、vuser_end中被使用。例子:
#ifndef _GLOBALS_H #define _GLOBALS_H //------------------------------------------------------------------- // Include Files #include "lrun.h" #include "web_api.h" #include "lrw_custom_body.h" //定义全局变量char * str="This is a test"; //------------------------------------------------------------------- // Global Variables#endif // _GLOBALS_H |
LR中参数的定义和赋值
1.参数的赋值和取值
lr_save_string("hello world","param");
lr_eval_string("{param}");
2.变量到参数
int x; char *y; y="hello"; x=10; lr_save_int(x,"param"); lr_save_string(y,"param1"); lr_eval_string("{param}"); lr_eval_string("{param1}"); |
3.变量读参数
char x[100]; x="{param}"; lr_save_string("hello world","param"); lr_eval_string(x); |
LR中文件下载模拟
Action() { //定义一个整型变量保存获得文件的大小 int flen; //保存文件句柄 long filedes; //保存文件路径及文件名 char file[256]="/0"; //定义一个随机数 char * strNumber; //Returns the string argument after evaluating embedded parameters. strNumber=lr_eval_string("{RandomNum}"); //将路径保存到file变量中 strcat(file,"D://Excel//"); //将获得的随机数拼接在file变量之后 strcat(file,strNumber); //将下载的文件后缀名.rar拼接在file变量之后 strcat(file,".rar"); //到此为止一个LoadRunner已经获得了一个完整的文件名 //设置参数的最大长度,注意该值必须大于文件的大小 web_set_max_html_param_len("20000"); //使用关联函数获取下载文件的内容,在这里不定义左右边界,获得服务器响应的所有内容 web_reg_save_param("fcontent", "LB=", "RB=", "SEARCH=BODY", LAST); lr_start_transaction("下载帮助文件"); //发送下载帮助文件的请求 web_url("DownLoadServlet", "URL=https://testserver3/servlet/DownLoadServlet?filename=help", "Resource=1", "RecContentType=application/x-msdownload", "Referer=", "Snapshot=t41.inf", LAST); //获取响应中的文件长度 flen = web_get_int_property(HTTP_INFO_DOWNLOAD_SIZE); if(flen > 0) { //以写方式打开文件 if((filedes = fopen(file, "wb")) == NULL) { lr_output_message("打开文件失败!"); return -1; } //写入文件内容 fwrite(lr_eval_string("{fcontent}"), flen, 1, filedes); //关闭文件 fclose(filedes); } lr_end_transaction("下载帮助文件",LR_AUTO); return 0; } |
如何进行数据的关联
VuGen提供二种方式帮助您找出需要做关联(correlation)的值:
1. 自动关联
2. 手动关联
自动关联
VuGen内建自动关联引擎(auto-correlationengine),可以自动找出需要关联的值,并且自动使用关联函数建立关联。有两种方式:
RuleCorrelation
可以在【RecordingOptions】>【InternetProtocol】>【Correlation】中启用关联规则,则当录制这些应用系统的脚本时,VuGen会在脚本中自动建立关联。
您也可以在【RecordingOptions】>【InternetProtocol】>【Correlation】检视每个关联规则的定义。
请依照以下步骤使用RuleCorrelation:
1. 启用auto-correlation
1) 点选VuGen的【Tools】>【RecordingOptions】,开启【RecordingOptions】对话窗口,选取【InternetProtocol】>【Correlation】,勾选【Enablecorrelation during recording】,以启用自动关联。
2) 假如录制的应用系统属于内建关联规则的系统,如AribaBuyer、BlueMartini、BroadVision、InterStage、mySAP、NetDynamics、Oracle、PeopleSoft、Siebel、SilverJRunner等,请勾选相对应的应用系统。
3) 或者也可以针对录制的应用系统加入新的关联规则,此即为使用者自订的关联规则。
2. 录制脚本
开始录制脚本,在录制过程中,当VuGen侦测到符合关联规则的数据时,会依照设定建立关联,您会在脚本中看到类似以下的脚本,此为BroadVision应用系统建立关联的例子,在脚本批注部分可以看到关联前的数据为何。
3. 执行脚本验证关联是OK的。
CorrelationStudio
当录制的应用系统不属于VuGen预设支持的应用系统时,RuleCorrelation可能既无法发挥作用,这时可以利用CorrelationStudio来做关联。
CorrelationStudio会尝试找出录制时与执行时,服务器响应内容的差异部分,藉以找出需要关联的数据,并建立关联。
使用CorrelationStudio的步骤如下:
1. 录制脚本并执行
2. 执行完毕后,VuGen会跳出下面的【ScanAction for Correlation】窗口,询问您是否要扫描脚本并建立关联,按下【Yes】按钮。
3. 扫描完后,可以在脚本下方的【CorrelationResults】中看到扫描的结果。
4. 检查一下扫瞄的结果后,选择要做关联的数据,然后按下【Correlate】按钮,一笔一笔做,或是按下【CorrelateAll】让VuGen一次就对所有的数据建立关联。
注意:由于CorrelationStudio会找出所有有变动的数据,但是并不是所有的数据都需要做关联,所以不建议您直接用【CorrelateAll】。
5. 一般来说,您必须一直重复步骤1~4直到所有需要做关联的数据都找出来为止。因为有时前面的关联还没做好之前,将无法执行到后面需要做关联的部份。
手动关联
手动关联的执行过程大致如下:
1. 使用相同的业务流程与数据,录制二份脚本
2. 使用Diff工具比较一下生成的Action.c,找出需要关联的数据
3. 使用web_reg_save_param函数手动建立关联
4. 将脚本中有用到关联的数据,以参数取代
注意动态数据不能放在vuser_init.c文件中。不是所有的差异处都要关联的,WEB_URL()等方法的参数列表的先后顺序是没关系的,思考时间当然也可以不同。
LR的检查点设置
常用检查点函数如下:
1)web_find()函数用于从 HTML 页中搜索指定的文本字符串;
2)web_reg_find()函数注册一个请求,以在下一个操作函数(如 web_url)检索到的HTML网页上搜索指定的文本字符串;
3)web_image_check()函数用于从HTML页面中查找指定的图片;
4)web_global_verfication()属于注册函数,注册一个在web页面中搜索文本字符串的请求,与web_reg_find只在下一个Action函数中执行搜索不同的是它在之后所有的Action类函数中执行搜索指定的文本字符串;
下面分别介绍以上函数的用法:
1、web_find()函数参数举例:
web_find("web_find","RighOf=a","LeftOf=b","What=name",LAST);
参数解释:"web_find"定义该查找函数的名称;“LeftOf”和“RighOf=”用来定义查找字符的左右边界;“What=”定义查找内容;
例如上述参数举例中的意思就是在页面中查找左边界为b,右边界为a,内容为name的信息;
使用该函数注意事项:该函数是在查找页面中的内容,所以要放在要查找的内容的后面;该函数只能在基于HTML模式录制的脚本中进行查找
注意事项:使用该函数时,要在Vuser->Run-Tme Settings中更改下设置
勾选Enable Image and text check
系统默认是不勾选该选项的。
2、web_reg_find()函数参数举例:
web_reg_find("Search=Body","SaveCount=ddd","Test=aaa",LAST);
参数解释: Search用来定义查找范围,SaveCount定义查找计数变量名称,该参数可以记录在缓存中查找内容出现的次数,可以使用该值,来判断要查找的内容是否被找到;
例如上述参数举例中的意思就是Body中查找内容为aaa的信息,并将出现次数记录在变量ddd中;
【代码一:web_reg_find("Text=PaymentDetails",LAST);
代码思路:1.“Payment Details” 为你要检查的文本;
2. 脚本执行到此处,若在页面上找到了这几个字符串,那脚本继续执行下去;若没有找到,脚本将在此报错并且结束。
3、web_image_check()函数参数说明:
web_image_check("web_image_check","Alt=","Src=",LAST);
参数解释:“Alt”和“Src”的值直接取该图片在网页源代码中相应参数的值;
注意事项:使用该函数时,要在Vuser->Run-Tme Settings中勾选Enable Image and text check,具体操作请看web_find()中的注意事项。
插入函数的方法
1、 手工写入,在需要插入函数的位置手工写入该函数;
2、 光标停留在要插入函数的位置,在INSERT菜单中,选择new step,在列表中选择或查找要插入的函数,根据提示填写必要的参数;
3、 在tree view模式下,在树状菜单中选中要插入函数的位置,右键,选择insert after或insert before,根据提示填写必要的参数;
总结:
1、 这两个函数函数类型不同,WEB_FIND是普通函数,WEB_REG_FIND是注册函数;
2、 WEB_FIND使用时必须开启内容检查选项,而WEB_REG_FIND则不没有此限制;
3、 WEB_FIND只能用在基于HTML模式录制的脚本中,而WEB_REG_FIND没有此限制;
4、 WEB_FIND是在返回的页面中进行内容查找,WEB_REG_FIND是在缓存中进行查找;
5、 WEB_FIND在执行效率上不如WEB_REG_FIND;
使用web_concurrent_start报错
录制方式选的是HTML-based script,改为url-based script就可以了。该函数不支持HTML-based script方式。
LR中的脚本变量声明报错
Loadrunner使用的是C语言的语法。脚本中变量必须在开头声明。比如说,在Vuser_init、Action、Vuser_end定义变量,必须现在最顶端生命,后面才能使用。如果不这样,编译器是通不过的。
创建文本文件
Set bjFSO = CreateObject("Scripting.FileSystemObject")
Set bjFile = objFSO.CreateTextFile("C:\FSOScriptLog.txt")
检察文件是否存在
Set bjFSO = CreateObject("Scripting.FileSystemObject") If objFSO.FileExists("C:\FSOScriptLog.txt") Then Set bjFolder = objFSO.GetFile("C:\FSOScriptLog.txt") Else MsgBox "File does not exist." End If |
删除文本文件
Set bjFSO = CreateObject("Scripting.FileSystemObject")
objFSO.DeleteFile("C:\FSOScriptLog.txt")
Set bjFSO = CreateObject("Scripting.FileSystemObject")
objFSO.MoveFile "C:\FSOScriptLog.txt" , "D:\"
复制文件
Set bjFSO = CreateObject("Scripting.FileSystemObject")
objFSO.CopyFile "C:\FSOScriptLog.txt" , "D:\"
重命名文件
Set bjFSO = CreateObject("Scripting.FileSystemObject")
objFSO.MoveFile "C:\FSO\ScriptLog.txt" , "C:\FSO\BackupLog.txt"
读取全部内容
Const ForReading = 1 Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjFile = objFSO.OpenTextFile("C:ScriptsTest.txt", ForReading) strContents = objFile.ReadAll Wscript.Echo strContents objFile.Close |
一行行的读取文本文件内容
Const ForReading = 1 Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjTextFile = objFSO.OpenTextFile ("C: \ScriptsTest.txt", ForReading) Do Until objTextFile.AtEndOfStream strComputer = objTextFile.ReadLine Wscript.Echo strComputer Loop objTextFile.Close |
追加文本文件一行内容
Const ForAppending = 8 Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjTextFile = objFSO.OpenTextFile ("C:\ScriptsTest.txt ", ForAppending, True) objTextFile.WriteLine("追加的内容") objTextFile.Close |
有用的几个函数:
替换:将Jim替换成James。
strNewText = Replace(strText, "Jim ", "James ")
用逗号分隔字符串:
arrpath=split(strDN,",")
wscript.echo arrpath(0)
读取文本文件指定的行内容(读第四行内容存到strLine变量中)
Const ForReading = 1 Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjTextFile = objFSO.OpenTextFile("C:\ScriptsTest.txt ", ForReading) For i = 1 to 3 objTextFile.ReadLine Next strLine = objTextFile.Readline MsgBox strLine objTextFile.Close |
查看文件属性
Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjFile = objFSO.GetFile("c:\ScriptsTest.txt") msgbox "Date created: " & objFile.DateCreated msgbox "Date last accessed: " & objFile.DateLastAccessed msgbox "Date last modified: " & objFile.DateLastModified msgbox "Drive: " & objFile.Drive msgbox "Name: " & objFile.Name msgbox "Parent folder: " & objFile.ParentFolder msgbox "Path: " & objFile.Path msgbox "Short name: " & objFile.ShortName msgbox "Short path: " & objFile.ShortPath msgbox "Size: " & objFile.Size msgbox "Type: " & objFile.Type |
修改文件属性
Set bjFSO = CreateObject("Scripting.FileSystemObject") Set bjFile = objFSO.GetFile("C:\ScriptsTest.txt") If objFile.Attributes AND 1 Then objFile.Attributes = objFile.Attributes XOR 1 End If |
写入文本文件
Const ForWriting=2 Set bj=createobject("scripting.filesystemobject") Set bjfile=obj.opentextfile("C:\ScriptsTest.txt", ForWriting) objfile.write("This is line 1.") objfile.writeline("This is line2.") objfile.close |
版权声明:本文出自 wpt1987 的51Testing软件测试博客:http://www.51testing.com/?365260
fabric
title是开发,但是同时要干开发
测试还有运维的活……为毛 task*3 不是 salary * 3 (o(╯□╰)o)
近期接手越来越多的东西,发布和运维的
工作相当机械,加上频率还蛮高,导致时间浪费还是优点多。
修复bug什么的,测试,提交版本库(2分钟),ssh到测试环境pull部署(2分钟),rsync到线上机器A,B,C,D,E(1分钟),分别ssh到ABCDE五台机器,逐一重启(8-10分钟) = 13-15分钟
其中郁闷的是,每次操作都是相同的,命令一样,要命的是在多个机器上,很难在本机一个脚本搞定,主要时间都浪费在ssh,敲命令上了,写成脚本,完全可以一键执行,花两分钟看下执行结果
直到,发现了fabric这货
作用
很强大的工具
可以将自动化部署或者多机操作的命令固化到一个脚本里
和某些运维工具很像,用它主要是因为,python…..
简单好用易上手
当然,
shell各种命令组合起来也可以,上古神器和现代兵器的区别
环境配置
在本机和目标机器安装对应包(注意,都要有)
sudo easy_install fabric
目前是1.6版本(或者用pip install,一样的)
安装完后,可以查看是否安装成功
[ken@~$] which fab
/usr/local/bin/fab
装完之后,可以浏览下官方文档
然后,可以动手了
hello world
先进行本机简单操作,有一个初步认识,例子来源与官网
新建一个py脚本: fabfile.py
def hello():
print("Hello world!")
命令行执行:
[ken@~/tmp/fab$] fab hello
Hello world!
Done.
注意,这里可以不用fabfile作为文件名,但是在执行时需指定文件
[ken@~/tmp/fab$] mv fabfile.py test.py fabfile.py -> test.py [ken@~/tmp/fab$] fab hello Fatal error: Couldn't find any fabfiles! Remember that -f can be used to specify fabfile path, and use -h for help. Aborting. [ken@~/tmp/fab$] fab -f test.py hello Hello world! Done. |
带参数:
修改fabfile.py脚本:
def hello(name, value):
print("%s = %s!" % (name, value))
执行
[ken@~/tmp/fab$] fab hello:name=age,value=20 age = 20! Done. [ken@~/tmp/fab$] fab hello:age,20 age = 20! Done. |
执行本机操作
简单的本地操作:
from fabric.api import local def lsfab(): local('cd ~/tmp/fab') local('ls') |
结果:
[ken@~/tmp/fab$] pwd;ls /Users/ken/tmp/fab fabfile.py fabfile.pyc test.py test.pyc [ken@~/tmp/fab$] fab -f test.py lsfab [localhost] local: cd ~/tmp/fab [localhost] local: ls fabfile.py fabfile.pyc test.py test.pyc Done. |
实战开始:
假设,你每天要提交一份配置文件settings.py到版本库(这里没有考虑冲突的情况)
如果是手工操作:
cd /home/project/test/conf/
git add settings.py
git commit -m 'daily update settings.py'
git pull origin
git push origin
也就是说,这几个命令你每天都要手动敲一次,所谓daily job,就是每天都要重复的,机械化的工作,让我们看看用fabric怎么实现一键搞定:(其实用shell脚本可以直接搞定,但是fab的优势不是在这里,这里主要位后面本地+远端操作做准备,毕竟两个地方的操作写一种脚本便于维护)
from fabric.api import local
def setting_ci():
local("cd /home/project/test/conf/")
local("git add settings.py")
#后面你懂的,懒得敲了…..
混搭整合远端操作
这时候,假设,你要到机器A的/home/ken/project对应项目目录把配置文件更新下来
#!/usr/bin/env python # encoding: utf-8 from fabric.api import local,cd,run env.hosts=['user@ip:port',] #ssh要用到的参数 env.password = 'pwd' def setting_ci(): local('echo "add and commit settings in local"') #刚才的操作换到这里,你懂的 def update_setting_remote(): print "remote update" with cd('~/temp'): #cd用于进入某个目录 run('ls -l | wc -l') #远程操作用run def update(): setting_ci() update_setting_remote() |
然后,执行之:
[ken@~/tmp/fab$] fab -f deploy.py update [user@ip:port] Executing task 'update' [localhost] local: echo "add and commit settings in local" add and commit settings in local remote update [user@ip:port] run: ls -l | wc -l [user@ip:port] out: 12 [user@ip:port] out: Done. |
注意,如果不声明env.password,执行到对应机器时会跳出要求输入密码的交互
多服务器混搭
操作多个服务器,需要配置多个host
#!/usr/bin/env python # encoding: utf-8 from fabric.api import * #操作一致的服务器可以放在一组,同一组的执行同一套操作 env.roledefs = { 'testserver': ['user1@host1:port1',], 'realserver': ['user2@host2:port2', ] } #env.password = '这里不要用这种配置了,不可能要求密码都一致的,明文编写也不合适。打通所有ssh就行了' @roles('testserver') def task1(): run('ls -l | wc -l') @roles('realserver') def task2(): run('ls ~/temp/ | wc -l') def dotask(): execute(task1) execute(task2) |
结果:
[ken@~/tmp/fab$] fab -f mult.py dotask [user1@host1:port1] Executing task 'task1' [user1@host1:port1] run: ls -l | wc -l [user1@host1:port1] out: 9 [user1@host1:port1] out: [user2@host2:port2] Executing task 'task2' [user2@host2:port2] run: ls ~/temp/ | wc -l [user2@host2:port2] out: 11 [user2@host2:port2] out: Done. |
扩展
1.颜色
可以打印颜色,在查看操作结果信息的时候更为醒目和方便
from fabric.colors import *
def show():
print green('success')
print red('fail')
print yellow('yellow')
#fab -f color.py show
2.错误和异常
关于错误处理
默认,一组命令,上一个命令执行失败后,不会接着往下执行
失败后也可以进行不一样的处理, 文档
目前没用到,后续用到再看了
3.密码管理
看文档
更好的密码管理方式,哥比较土,没打通,主要是服务器列表变化频繁,我的处理方式是:
1.host,user,port,password配置列表,所有的都写在一个文件
或者直接搞到脚本里,当然这个更........
env.hosts = [ 'host1', 'host2' ] env.passwords = { 'host1': "pwdofhost1", 'host2': "pwdofhost2", } 或者 env.roledefs = { 'testserver': ['host1', 'host2'], 'realserver': ['host3', ] } env.passwords = { 'host1': "pwdofhost1", 'host2': "pwdofhost2", 'host3': "pwdofhost3", } |
2.根据key解析成map嵌套,放到deploy中
另外命令其实也可以固化成一个cmds列表的…