前几天我在微博上发出了一个STB-010(
软件测试在线公益课程系列)报名通知的帖子,这一讲的题目是“软件测试黒盒技术与应用 - 状态转换测试方法”,立即引来了一些讨论。
比如朱少民老师就指出:“人们喜欢将软件测试分为白盒、黑盒方法,虽然方法论上是成立的,从完全不同的方式去看待SUT,有其不同的应用场景,但是这种划分越来越感觉不合适,如将等价类划分、边界值分析...等归为黑盒方法,而它们完全可在代码测试上*透明*应用,如针对参数传递、内部变量等进行测试,是时候重新审视测试方法体系了。”
我个人对上述观点总体上是表示认同的。不过细微之处还可以深究下。
比如,“将等价类划分、边界值分析等归为
黑盒测试技术”与“它们完全可在代码测试上*透明*应用”,我并不认为这二者前后矛盾!“黑盒”是一种思想,将当前正在考虑的问题看作一个黑匣子,忽略其内部实现的细节,从外部考虑其行为表现。换句话说,此时我们正站在更高一级别的抽象层次上看待问题。
这个黑匣子可能很大,比如是整个被测系统;它也可以很小,比如对应到一个函数。因此,我们可以将各种黑盒
测试技术应用到整个系统层面,也可以在代码层面应用他们。实际上,“等价类划分、边界值分析”等只是一些测试技术,如何使用这些测试技术,完全取决于上下文,这个观点也是“上下文驱动测试学派(Context-Driven Testing School)”所倡导的。
有不少人仍然认为“只要在代码层面做测试,一定指的就是
白盒测试”。我们首先来看看一些白盒测试的定义。
ISTQB定义White-box Testing为“Testing based on an analysis of the internal structure of the component or system.”该定义认为,不论是在
单元测试阶段还是
系统测试阶段,只要关注的是被测对象内部的结构,就归为白盒测试。比如关心某段代码的各种测试覆盖情况(分支覆盖、条件覆盖等),属于白盒测试的范畴,这点相信大多数人能够理解。但是另外一些情况也许就不那么容易理解了,比如在GUI测试中,测试人员检查菜单调用的是否正确(主菜单和各层子菜单的嵌套调用情况),而不关心每个菜单内容的正确性与否时,就是在做白盒测试了。
我个人觉得这样理解白盒测试有些牵强。菜单的调用结构是否正确,也不妨认为是系统
功能测试的一部分,此时也是把系统当做黑盒来测试的。如果测试人员通过阅读分析代码来判断这种菜单调用的具体实现是否正确时,这倒可以归为白盒测试。当然,不论黑盒、白盒,只是个概念而已,重要的是想清楚在什么时候应该做什么样的测试,应该完成哪些具体的测试活动。
再回到前面的问题,“只要在代码层面做测试,一定指的就是白盒测试”吗?答案是否定的。原因有二:如果是通过阅读代码来了解SUT,那么代码只是一种
学习的手段,就像有时需要通过阅读文档来了解被测系统一样,看过代码之后自然可以做黑盒测试;对于任何一个函数或者类,如果把它作为一个黑匣子,考虑它实现的主要功能时,使用等价类或边界值等测试技术对它进行分析,那么我们自然是在做黑盒测试。
可见,是做黑盒还是白盒测试,与是否阅读代码没有直接的关系。假如黑盒测试是把被测对象当作一个黑匣子来看,那么白盒测试就是打开这个黑匣子,深入内部挖掘细节的测试过程。当然,由于代码内部包含很多实现细节,基于代码做测试的时候,往往比纯粹的基于需求文档做测试想到更多的测试点,也有人把这称为介于黑盒与白盒之间的所谓灰盒测试。
“黑盒”与“白盒”的抽象层次
既然“黑盒”与“白盒”分属于不同的抽象层次,而抽象又是分很多层的,那么当我们讨论某测试是黑盒还是白盒时,就离不开具体的测试上下文了,因为所谓的“黑盒”与“白盒”是相对而言的。这就像当我们讨论“集成测试”时,此“集成测试”非彼“集成测试”,一个系统中从单元、模块、子系统、系统等各个层次的接口很多,讨论的双方一定要先明确是在哪个层次上讨论“集成测试”的问题,这样讨论的结果才有意义。
“黑盒”与“白盒”分属于不同的抽象层次上,测试人员应该更多关注黑盒测试,还是更多关注白盒测试,或者二者都应给予足够的关注?
朱老师的另外一个帖子:“当系统复杂时有必要简化问题,减少内部干扰,把系统看作一个整体(黑盒),搞清楚系统边界、接口、环境。只有此时(宏观层面上)黑盒方法是有意义的。而我们不能认识系统、无法理解系统/业务结构的时代可以过去了,我们应能够彻底搞清楚代码、数据、业务及其结构、控制流等,软件测试才是有效、充分的。”
如果这段话中的“我们”泛指的是“测试团队”,我深表认同。对于每一个测试人员是否也要黑盒白盒通吃,可能要根据具体的测试上下文(产品和项目复杂度、人员技能水平等)来确定了,这属于一个测试项目具体的测试策略的问题。但是这并不妨碍每个测试人员把掌握黑盒与白盒测试技术作为自己努力的目标和方向。
为什么说“同时做好黑盒与白盒测试,软件测试才是有效、充分的呢?”举个例子来说明。
有这样一段代码,输出0~99共100位数字:
int x;
for(x=0; x<100; x++);
cout<<x;
结果由于程序员粗心,多加了一个分号,导致实际输出的只是100而已。这种错误是比较常见的,如果把这一小段代码呈现在我们面前,我们用代码review的方式很容易就能找出这个bug。但是现在试想一下,把这段代码放到一个有几百万行代码甚至更大的一个系统中,通过代码review找出这个bug,无异于大海捞针!
况且,增加一个多余的分号并没有违反什么语法规则,编译器也只是会认为这是一个循环体为空的循环语句而已,并不会有错误提示。
您可能会说,这简单,用黑盒测试的方法运行一遍基本功能,很容易就发现了。但是,谁又能保证黑盒测试可以覆盖所有的bug呢?毕竟,测试是无穷无尽的,永远也执行不完,而漏测是必然的。
可见,单单用白盒或者黑盒的方法都不是绝对保险的,并且黑盒测试和白盒测试由于他们的视角不一样、所处的抽象层次不一样,能够发现的缺陷类型、发现缺陷的难易程度、发现缺陷的成本多少、对测试人员的技能要求等都不一样,有效地将二者结合起来使用、互为补充,才能使测试效果最优。
结论
不必太在意“黑盒测试”与“白盒测试”具体的定义,他们只是辅助我们做好实际测试工作的一些概念而已(您甚至可以有自己的一套定义、一套术语);
“黑盒”与“白盒”体现的是测试人员思考问题的一种方式,“黑盒”将被测对象当作一个黑匣子,考虑其整体表现;“白盒”将一个个黑匣子打开,深入内部探索细节。
大体上,“黑盒测试”与“白盒测试”分属于不同的抽象层次,黑盒更偏重于概括一些,白盒更偏重于具体一些;
“黑盒”与“白盒”的抽象层次是相对的,一个系统可能有多层的抽象;
“黑盒测试”与“白盒测试”二者结合起来使用,互为补充,测试才更充分。
性能环境,也是困扰
性能测试人员很重要的一个问题。如何模拟线上真实的环境?如何在测试环境进行的性能测试结果,能准确的反应到生产线上去?
先聊下我们的做法。
首先确认线上的网络拓扑图。比如:
左边是线上环境,线上一般是分布式集群部署。比如用户访问服务器A,而服务器A需要依赖到服务器B和C提供的服务,而服务器C与DB存储打交道。那么在性 能测试环境,为了模拟用户的行为,需要搭建服务器A,B,C和DB各一台。通过对单台服务器A的压测结果TPS,再线性乘以机器数量,完成线上服务器A集 群的TPS的评估。
这样做,简单的完成了线上和线下的结果推算。但是这样做,有一个前提:就是保证线上和性能测试环境的机器配置是一样的。机器的配置分为2个部分:硬件和软件。
硬件方面,比如机器是4核4G的xen虚拟机,千兆网卡,SATA磁盘。
软件方面,包括
操作系统的版本,位数,比如:redhat 5.4,**位机器;JDK版本:openJDK 1.6.0;JVM参数配置;
web容器:tomcat7.
上面说的是应用层面,性能测试还有很重要的一步,就是性能测试数据的准备。
性能测试
数据库的环境也与应用层一样,保持和线上一样的硬件与软件配置。这里有个问题就是:生产线上一台数据库,对应多台应用。而在性能测试环境中,被我们映射成1:1的关系后,很难找到数据库层的性能瓶颈,因为在压测过程中,应用层会比数据层更快到达性能瓶颈。
接下来,简单描述下性能测试数据的准备。性能测试数据的准备也分为2个部分。1是业务数据,2是基础数据。
什么是业务数据?业务数据就是性能测试要模拟的用户拥有的数据。比如卖家A,有100个商品。100就是业务数据。基础数据就是商品所在表的总数据量。比如100W。
性能测试的数据,不光要关注业务,更要关注数据库表的量级,因为100条数据,和100W条数据,性能测试出来完全是两个结果。
为了数据的准确性,性能测试的数据表,也要满足生产线上表的量级。
满足了这些条件,性能环境才能基本真实模拟生产线的环境,测试出来的结果才有推算上线后结果的可能。
后记:
每个公司的业务不同,系统架构也不同,所以性能测试环境方面肯定有很大的差异。我只是简单的写了我所在公司的性能环境的一些知识,分享给大家,供大家一个参考,希望你能从这里获得有用的东西。关于性能测试环境,我的一些观点:
1. 如果允许生产线做性能测试,就尽量做生产线性能测试。同样的环境,性能测试的结果才有意义,性能测试的价值也最大体现。
2. 测试环境的配置可以与线上一样,可以做下N:1的映射。结果线性乘以机器数,可以评估线上集群的负载。
3. 真没条件,线上线下环境差异很大。就尽量找一些代码级别的性能瓶颈。优化后再上线,线下的结果就不用去推算线上,很不科学
例如我们在Web Api项目中有个Controller
public class SomeController : ApiController { public HttpResponseMessage Get() { // 一些操作 return Request.CreateResponse(HttpStatusCode.OK, someModel); } } |
如果你在
单元测试中直接调用 SomeController 的Get()方法,那么你将会收到一个Exception提示Request为Null。
因此我们需要在测试代码中构造一个Request,有两种方法
1、简单构造法
[TestMethod] public void UnitTestMethod() { // 环境准备部分 YourNameSpace.Controllers.SomeController controller = new SomeController(); // 下面两个语句是构造一个简单的请求报文 controller.Request = new HttpRequestMessage(); controller.Request.SetConfiguration(new HttpConfiguration()); var result = controller.Get(); // 断言 } |
2、可控性更强的构造
[TestMethod] public void UnitTestMethod() { // 环境准备部分 YourNameSpace.Controllers.SomeController controller = new SomeController(); var config = new HttpConfiguration(); var request = new HttpRequestMessage(HttpMethod.Post, "YourUrl"); var route = config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}"); var routeData = new HttpRouteData(route, new HttpRouteValueDictionary { { "controller", "products" } }); controller.ControllerContext = new HttpControllerContext(config, routeData, request); controller.Request = request; controller.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config; var result = controller.Get(); // 断言 } |
然后就可以运行单元测试查看结果了。
6年前,我在一家中型跨国公司
工作的时候,我建议与其浪费时间在准备充分的
测试用例,还不如编写描述测试场景的文档。所有的人都对我的建议。投以烦恼的目光。他们的脸上清晰地传递出,我这个建议犯了个大错误。虽然没人否认了这一想法,但更没有人愿意接受。每个人都认同传统做法,即编写测试用例,会更稳妥。我无力反驳。
4年之后,该公司接到一个测试项目,唯一的约束就是时间,唯一的期望是完整的测试证明材料。再次见面,我们讨论了怎么赶上最后期限的想法。应用程序主要是关于“通过搜索和生成不同菜单项的报表”。编写和记录测试用例应该要花大部分时间,我们不确定,有多少文档会用到客户交付时。所以,我建议记录测试场景,虽然有些犹豫,但最后大家都还是同意了。相对说我们可以节省宝贵的文档时间,更可以利用它进行测试。
测试用例很快就会被测试场景代替吗?
随着时间的推移,一切都在变化,软件行业和过程也发生了深刻的变化。
传统的瀑布式和v-模型已经被
敏捷和迭代模型所取代。文档是必要的,但是为了赶上最后期限,使过程简单而透明的,文档的方式也是可以改变的。
这些时候测试用例的文档是很重要的:
1.客户要求的文档(是项目的一部分)
2.没有时间的约束(我不认为这是可能的)
3.测试员是新人的或不熟悉产品
4.公司政策(我坚信它可以改变)
让我分享一下我的一个经历
我和我的团队参与测试一个项目是世界500强公司,有着灵活的时间表。我们用最好的模板来记录测试用例并且得到客户的评审通过。一旦开始组建QA团队,每天的大部分时间里,我们的责任就是,机械地遵循100个测试用例,更新文档中通过/失败结果,和在一天结束的时候把结果发给客户。很多团队的成员开始抱怨工作的单调乏味,尽管这仍然可以为公司创收。
然后就可以休息一天,没有新的测试。我们坐在一起,并讨论我们接下来要做什么。当我提议去想更多的方法来改进测试用例文档,所有的团队成员都否认我们投入的努力。按照他们的想法,他们没有更多地去思考我们已经覆盖的所有场景。说服他们跳出思维框架,产生更多的想法真的很艰难。
大多数时候,当我们测试用例(带执行结果)文档时,一旦得到客户的评审通过,就认为我们所做的工作已经完成,我们的大脑会自动停止思考任何其他方法来测试产品。
相信我,当测试用例文档准备好了,我们就只想机械地跟随它。请告诉我,在你的职业生涯中,你和团队成员在得到类似评审通过后,还提供了额外的测试用例的情况,你经历了过多少次?
另一个经历
在每周的团队挑战活动中,我们会要求团队成员完成对被测应用程序指定的“测试场景”。所有的团队成员,包括那些后期有反馈或无反馈的想法(有的没的的想法)。为什么呢?没有正式的用例文档了,他们就不得不“诸如:补填预期结果,每个步骤的每个用例的功能和前提条件等等”。我们在一天中居然收集了40个测试场景,这是一个成功的经历。
为了更好地证明我的经验,我想展示一个例子。
拿一个应用程序做示例:用一个用户名、密码,登录和取消按钮的登录页面来说。如果以同样的要求写测试用例,我们将通过结合不同的选项和细节,的排列组合最终可以写得50多个测试用例。
但如果用测试场景编写,这将是如下重要的10行:
High Level 的场景:登录功能
Low Level的场景:
1.检查应用程序的启动
2.检查登录页面上的文本内容
3.检查用户名字段
4.检查密码字段
5.检查登录按钮和取消按钮功能
参见= > 180 +示例测试场景来测试web和桌面应用程序。
由于时间关系,测试场景与其说是以前那个IODEX(一种去痛膏),还不如说是止痛药喷雾。但效果还是一样的。
最后,我总结的区别如下:
最后这篇文章应该得出的结论为:
测试用例是软件开发生命周期中最重要的一部分,没有了它,就很难跟踪、了解,遵循和推理出一些东西。但在 Agile的时代,测试用例将很快被测试场景所取代。
每个类型的测试的常见测试清单为 (数据库测试、GUI测试、功能测试等),加上测试场景,就是软件测试人员的现代利器。讨论、培训、问题和实践绝对可以改变你的生产力(包括报bug的能力)。
关于作者:这篇优秀的文章是由 STH的作者Bhumika Mehta。她是一个有着超过7年的软件测试项目管理经验。她喜欢测试存在的一切,欣赏好的创意和创新,但讨厌单调的工作。
像往常一样,我们欢迎听到您的想法。
本
文章记录我在linux系统下常用或有用的系统级命令,包括软硬件查看、修改命令,有CPU、内存、硬盘、网络、系统管理等命令。但本文不打算介绍生僻命令,也不介绍各个linux发行版下的特有命令,且以后会持续更新。
说明,我是在一个Centos 6.4 64位的虚拟机系统进行
测试。本文介绍的命令都会在此Centos下运行验证(也有部分命令会在我的suse/ubuntu系统里测试的,会做特明说明),但运行结果就不再列出了。
硬件篇
CPU相关
lscpu #查看的是cpu的统计信息.
cat /proc/cpuinfo #查看CPU信息详细信息,如每个CPU的型号,主频等
内存相关
free -m #概要查看内存情况 这里的单位是MB
cat /proc/meminfo #查看内存详细信息
磁盘相关
lsblk #查看硬盘和分区分布,显示很直观
df -h #查看各分区使用情况
cat /proc/partitions #查看硬盘和分区
mount | column -t #查看挂接的分区状态
网卡相关
lspci | grep -i 'eth' #查看网卡硬件信息
ifconfig -a #查看系统的所有网络接口
ethtool eth0 #如果要查看某个网络接口的详细信息,例如eth0的详细参数和指标
软件篇
内核相关
uname -a #查看版本当前 操作系统内核信息) cat /proc/version #查看当前操作系统版本信息 cat /etc/issue #查看版本当前操作系统发行版信息 cat /etc/redhat-release #同上 cat /etc/SuSE-release #suse系统下才可使用 lsb_release -a #用来查看linux兼容性的发行版信息 lsmod #列出加载的内核模块 |
网络
ifconfig #查看所有网络接口的属性 iptables -L #查看防火墙设置 service iptables status #查看防火墙状态 service iptables stop #关闭防火墙 route -n #查看路由表 netstat -lntp #查看所有监听端口 netstat -antp #查看所有已经建立的连接 netstat -s #查看网络统计信息进程 netstat -at #列出所有tcp端口 netstat -au #列出所有udp端口 netstat -lt #只列出所有监听tcp端口 |
系统管理
top #查看系统所有进程的详细信息,比如CPU、内存等,信息很多! df -lh #查看硬盘大小及使用率 mount #挂接远程目录、NFS、本地共享目录到linux下 hostname #查看/修改计算机名 w #查看活动用户 id #查看指定用户信息 last #查看用户登录日志 cut -d: -f1 /etc/passwd #查看系统所有用户 cut -d: -f1 /etc/group #查看系统所有组 crontab -l #查看当前用户的计划任务服务 chkconfig –list #列出所有系统服务 chkconfig –list | grep on #列出所有启动的系统服务程序 rpm -qa #查看所有安装的软件包 uptime #查看系统运行时间、用户数、负载 /sbin/chkconfig --list #查看系统自动启动列表 /sbin/chkconfig –add mysql #把MySQL添加到系统的启动服务组里面 |
文件相关
ls -lht #列出一个文件夹下所有文件及大小、访问权限
du -sh <dir> #查看指定目录的大小
du -lh <dir> #查看指定目录及各文件的大小
ln -s #建立软链接
进程相关
pstree -p pid #查看一个进程下的所有线程 pstree -a #显示所有进程的所有详细信息,遇到相同的进程名可以压缩显示。 ps -ef #查看所有进程 kill -9 pid #杀死进程 kill all test #杀死进程 kill -9 `pgrep test` #杀死进程 ./test.sh & #使程序在后台运行 nohup ./test.sh & #使程序在后台运行 |
压缩解压缩
zip -r dir.zip dir file #将目录dir、文件file等压缩到zip包,
zip -re dir.zip dir file #创建zip包,且加密
unzip dir.zip #解压
tar -zcvf dir.tar.gz dir file #将目录dir、文件file等压缩到tar包
tar -xf dir.tar.gz #解压
screen命令
screen命令组最大的好处就是当你的shell退出或关闭后,你运行的服务不会关系,也就是说,我们可以在screen里开启一组服务,且不受终端断开的影响。
screen -S test #创建一个名字为test的screen screen -r test #打开名字为test的screen screen -r pid #打开进程号为pid的screen screen -ls #列出所有的screen ctrl + a,d #当在一个screen时,退出screen ctrl + a,n #当在一个screen时,切换到下一个窗口 ctrl + a,c #当在一个screen时,创建一个新的窗口 |
scp命令
scp local_file remote_username@remote_ip:remote_dir #拷贝本地文件到远程机器上
scp -r local_dir remote_username@remote_ip:remote_dir #拷贝本地整个目录到远程机器上
软件包安装管理命令
假设你想要安装的软件包叫做app,注意,这里的命令通常需要sudo或者root权限。
//centos系统、redhat系统 rpm -qa | grep app #查找本机是否安装了app; rpm -ivh app.rpm #假设你有app的rpm包,这样直接安装 sudo yum install app #否则就在线安装 yum update app #更新app rpm -e app #删除已安装的app包 //suse、opensuse系统 zypper search app #查找本机是否安装了app; zypper install app #安装 zypper update app #更新 zypper remove app #删除 zypper lr #列出所有已定义的安装源。 zypper ar #添加新安装源。 zypper rr #删除指定的安装源 zypper mr #修改指定的安装源 //ubuntu系统 apt-get install app #安装 apt-get update app #更新 apt-get remove app #删除 apt-cache search app #搜索软件包 dpkg -i app.deb #假设你有app的deb包,这样直接安装 |
请关注,后续随着使用的深入还会持续更新。
传入实体执行(可添加 修改 删除)事务。
IDbHelper dbHelper = new OracleHelper(ConfigHelper.GetConfigString("BusinessDbConnection")); bool result = true; try { dbHelper.BeginTransaction(); //主表 TE_AREAManager manager = new TE_AREAManager(dbHelper, userInfo); TE_AREAEntity tE_AREAEntity = manager.GetObject(dbHelper.SqlSafe(eatxtAREA_ID)); manager.Delete(tE_AREAEntity); //子表 TE_AREA_SUBManager submanager = new TE_AREA_SUBManager(dbHelper, userInfo); TE_AREA_SUBEntity tE_AREA_SUBEntity = submanager.GetObject(dbHelper.SqlSafe(eatxtAREA_ID)); submanager.Delete(tE_AREA_SUBEntity); //事务提交 dbHelper.CommitTransaction(); } catch(Exception ex) { //事务回滚 dbHelper.RollbackTransaction(); result=false; } |
可以传入sql语句执行事务
IDbHelper dbHelper = new OracleHelper(ConfigHelper.GetConfigString("BusinessDbConnection")); bool result = true; try { dbHelper.BeginTransaction(); string commandText = " DELETE FROM TE_AREA WHERE ID=" + dbHelper.SqlSafe(id); dbHelper.ExecuteNonQuery(sqlString); commandText = " DELETE FROM TE_AREA_SUB WHERE ID=" + dbHelper.SqlSafe(id); dbHelper.ExecuteNonQuery(commandText); dbHelper.CommitTransaction(); } catch(Exception ex) { dbHelper.RollbackTransaction(); result=false; } |
还可以同时传入实体,SQL语句
IDbHelper dbHelper = new OracleHelper(ConfigHelper.GetConfigString("BusinessDbConnection")); bool result = true; try { dbHelper.BeginTransaction(); //主表 TE_AREAManager manager = new TE_AREAManager(dbHelper, userInfo); TE_AREAEntity tE_AREAEntity = manager.GetObject(dbHelper.SqlSafe(eatxtAREA_ID)); manager.Delete(tE_AREAEntity); //子表 TE_AREA_SUBManager submanager = new TE_AREA_SUBManager(dbHelper, userInfo); TE_AREA_SUBEntity tE_AREA_SUBEntity = submanager.GetObject(dbHelper.SqlSafe(eatxtAREA_ID)); submanager.Delete(tE_AREA_SUBEntity); //执行SQL语句 string commandText = " DELETE FROM JINTIANDA WHERE ID=" + dbHelper.SqlSafe(id); dbHelper.ExecuteNonQuery(commandText); //事务提交 dbHelper.CommitTransaction(); } catch(Exception ex) { //事务回滚 dbHelper.RollbackTransaction(); result=false; } |
以上事务处理方法是基于吉日嘎拉通用权限管理组件底层的代码上实现的,dbHelper,兼容各种数据库事务处理,非常省心省事。
欢迎大家提供自己的使用经验,共同提高开发效率。
今天在书上看到一个作者提出一个问题“怎样通过编写
Java代码让Jvm崩溃”,我看了之后也不懂。带着问题查了一下,
百度知道里面有这样一个答案:
1 package jvm; 2 3 public class Crash { 4 public static void main(String[] args) { 5 6 //Object[] o = {“abc”};初始值赋值,不会有影响。 7 Object[] o = null; 8 9 while (true) { 10 o = new Object[] { o }; 11 //输出的话,jvm就不会崩溃。 12 //System.out.println(o); 13 } 14 } 15 } |
程序运行十几秒之后,控制台会出现这样的错误:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at jvm.Crash.main(Crash.java:10)
很明显,超出内存空间错误。
我将原程序随意改了一下,如赋初始值等,对程序无影响。
可是我将死循环中的o输出在控制台的时候,jvm居然一直都不崩,为什么输出的话,就不会超出内存空间呢?
我看来,原程序能够使Jvm崩溃,是因为死循环中,通过旧对象,不断创建出新的对象,即创造的对象是互相引用的,所以GC是不会回收它们的,造成堆栈溢出。
仿照这个例子,我写了一个简单的类,模仿例子程序中的数组,如下:
1 package jvm; 2 3 public class JvmBean { 4 5 JvmBean bean = new JvmBean(this); 6 7 public JvmBean(JvmBean bean){ 8 this.bean = bean; 9 } 10 } |
1 package jvm; 2 3 public class MyCrash { 4 5 public static void main(String[] args) { 6 JvmBean j = null; 7 while(true){ 8 j = new JvmBean(j); 9 //无论输出不输出,jvm都会崩溃 10 //System.out.println(j); 11 } 12 } 13 } |
结果便是控制台输出如下的错误:
Exception in thread "main" java.lang.StackOverflowError at jvm.JvmBean.<init>(JvmBean.java:5) at jvm.JvmBean.<init>(JvmBean.java:5) at jvm.JvmBean.<init>(JvmBean.java:5) at jvm.JvmBean.<init>(JvmBean.java:5) at jvm.JvmBean.<init>(JvmBean.java:5) |
一长串的"at jvm.JvmBean.<init>(JvmBean.java:5)",后面的被我省略了。
结果看来,同样也造成了jvm崩溃,可是错误类型跟例子程序的不同,说堆栈溢出错误,并且无论是否输出,错误都一样发生,为什么呢?
由于评论的两位老兄的热心指点,两个问题都水落石出了!
这里过一下整个流程。
第一个异常 结合天添老兄说的,Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at jvm.Crash.main(Crash.java:10)是因为程序无法申请到足够的内存的时候抛出的异常,Object数组o不断指向新的Object数组,数组元素是原来的Object数组,这使得Object维数越来越高。不断申请内存空间,最终导致超出jvm中堆的最大值。堆内存溢出。为什么输出打印,时间会延长呢?yahokuma老兄一言惊醒梦中人!输出打印的话,虚拟机并不是不会崩溃,而是崩溃的时间大大延长了。而崩溃时间延长其实是假象,是因为输出属于IO事件,每次输出CPU都被中断,IO很耗时,所以,感觉上才会时间延长。
第二个异常,yahokuma 老兄在下面评论中已经说的很清楚了,我这里搬过来——“类内部的静态属性 > 静态块 > 对象属性 > 构造方法。注意这一点,那就是说 bean属性会先于JvmBean的构造函数被初始化。在你main函数中,new一个 JvmBean的构造函数之前,类内部的JvmBean对象要优先被初始化,这个类内部的属性bean的内部同样也包含了一个JvmBean对象需要被初始化,成循环调用,造 成了栈溢出。”所以异常才会是这个——Exception in thread "main" java.lang.StackOverflowError
我把原JvmBean改一下
1 package jvm; 2 3 public class JvmBean { 4 5 JvmBean bean = null; 6 7 public JvmBean(JvmBean bean){ 8 this.bean = bean; 9 } 10 } |
这样最终得到的结果跟第一个例子一样了。
如何使Jvm崩溃呢?如果想使它堆内存空间不足,造成典型的内存泄漏,可以创建对象,使它们不断向深层次引用。产生Exception in thread "main" java.lang.OutOfMemoryError: Java heap space 这样的错误。如果想使他们栈空间不足,最简单的,就是在方法里,如构造方法里不断申请新的内存空间就够了,如我第二个错误例子的示范。
monkeyrunner主要包括三个模块:
1、MonkeyRunner:这个类提供了用于连接monkeyrunner和设备或模拟器的方法,它还提供了用于创建用户界面显示提供了方法。
2、MonkeyDevice:代表一个设备或模拟器。这个类为安装和卸载包、开启Activity、发送按键和触摸事件、运行
测试包等提供了方法。
3、MonkeyImage:这个类提供了捕捉屏幕的方法。这个类为截图、将位图转换成各种格式、对比两个MonkeyImage对象、将image保存到文件等提供了方法。
device=MonkeyRunner.waitForConnection()
#等等连接到设备,与模拟器连接,返回monkeydevice对象,代表连接的设备。没有报错的话说明连接成功。
参数1:超时时间,单位秒,浮点数。默认是无限期地等待。
参数2:串deviceid,制定的设备名称。默认为当前设备(
手机优先,比如手机通过USB线连接到PC、其次为模拟器)。
默认连接:device=MonkeyRunner.waitForConnection()
参数连接:device=MonkeyRunner.waitForConnection(1.0, 'emulator-5554‘)
正在运行程序的休眠时间设置:
MonkeyRunner.sleep(秒数,浮点数)
捕获屏幕:
result=device.takeSnapshot()
result.writeToFile("D:/result.png",'png')
MonkeyImage.writeToFile(参数1:输出文件名,也可以包括路径,参数2:目标格式)
写成功返回true,否则返回false
发送指定键的关键事件:
device.press(参数1:键码, 参数2:触摸事件类型)
参数1:常用键内容
按下HOME键 device.press('KEYCODE_HOME', MonkeyDevice.DOWN_AND_UP)
按下BACK键 device.press('KEYCODE_BACK', MonkeyDevice.DOWN_AND_UP)
按下下导航键 device.press('KEYCODE_DPAD_DOWN', MonkeyDevice.DOWN_AND_UP)
按下上导航键 device.press('KEYCODE_DPAD_UP', MonkeyDevice.DOWN_AND_UP)
按下OK键 device.press('KEYCODE_DPAD_CENTER', MonkeyDevice.DOWN_AND_UP)
按下左导航键 device.press('KEYCODE_DPAD_LEFT', MonkeyDevice.DOWN_AND_UP)
按下右导航键 device.press('KEYCODE_DPAD_RIGHT', MonkeyDevice.DOWN_AND_UP)
相应的按键对应名称:
menu键:KEYCODE_MENU
home键:KEYCODE_HOME
back键:KEYCODE_BACK
search键:KEYCODE_SEARCH
call键:KEYCODE_CALL
end键:KEYCODE_ENDCALL
上音量键:KEYCODE_VOLUME_UP
下音量键:KEYCODE_VOLUME_DOWN
power键:KEYCODE_POWER
camera键:KEYCODE_CAMERA
(一)基于业务场景的分析
1. 背景分析
用户角色:数据的使用者,属于什么岗位,什么角色,关注点是什么。
应用场景:数据在哪些前端业务系统中使用,业务关系是怎样的。
使用目的:使用这些数据的目的是什么,能够为业务带来怎样的效益。
2. 业务场景分解
统计维度:针对每个维度的粒度,层次和成员等。
统计口径:针对哪个时间段的数据进行统计
统计指标:每个指标的定义及情景细分
计算逻辑:维度和指标的计算逻辑
更新频率:数据更新的频率。
3. 关于历史数据的考量
比如新增字段,历史数据是否需要补数。
(二)基于程序设计的分析
1. 存储过程
所在包名
调用方式:通常是定时程序,数据库job或
java quartz
功能说明:初始化逻辑和增量更新逻辑。
改动方式:新增或修改
入参出参
目标表
2. 数据表
结果表:表结构是否满足业务统计要求
中间表:a.用途;b.更新或删除的机制
源表:a.源数据是否能满足需求;b.源数据的分布;c.源表之间的关联关系
3. Java逻辑
接口类型:通常是查询接口
接口名称
改动方式:新增或修改
入参返回
实现逻辑
4. 存储过程和java接口的调用时机
存储过程的调用顺序和执行时间。
Java接口的调用时机和触发条件。
5. 初始化数据,增量更新数据,实时处理数据
初始化数据:对当前所有数据的处理结果。
增量更新数据:对每日变化数据的处理结果。
实时处理数据:业务系统产生业务数据的同时进行数据处理的结果。
注意:除了测试初始化数据以外,还需要模拟增量更新数据的处理和需要实时处理的数据。
(三)基于结果表的或基于检查点的分析
1. 列数据(字段):
检查点:
每个字段的取值赋值是否正确;
哪些场景下需要更新字段值,程序是否都有考虑;
增量跑的时候,是否会错误的更新不该更新的数据;
对列值进行分类,观察是否有特殊数据,给予特别关注。
2. 行数据(记录):
检查点:分类统计记录数,分析合理性。
(四)基于边界条件和边界数据的分析
这里只关注系统测试层面的边界条件和边界数据,对于代码层面的边界值测试建议在单元测试阶段完成,成本更低。
1. 边界条件
需要测试上边界和下边界,以及超越上下边界的情况。
在包含边界条件的程序中可能开始一直是大于号判断,到了下边界可能会忘了改成小于号判断。
2. 边界数据
对于值的范围,值的个数,有序集合,需要选取正好等于,刚刚大于,刚刚小于,或者有序集合的第一个和最后一个。
二. 关于后台数据逻辑的测试方法
(一)常规测试方法
1. 编写测试数据准备脚本(按维度)
查询出基于测试分析得到的各种测试数据类型
2. 编写测试数据验证脚本(按指标)
按指标计算逻辑直接查询得到或间接得到一个预期的结果值。
3. 查结果表
根据测试数据ID查结果表,得到一个实际的结果值。
4. 比较预期结果值和实际结果值
(二)基于存储过程的测试方法
是基于常规测试方法的分析基础上,将SQL打包成procedure,能够更快速的执行,并将测试结果记录到测试表中。
1. 拆分,从复杂到简单:
将复杂的逻辑拆分成若干个简单片段,保证每个片段的数值或属性值容易得到。
2. 建测试表:
在测试环境中,构建一张测试数据表,除ID外,为每个片段设置一个或几个表字段,用于存储每个片段得到的中间值。
3. 编写测试存储过程:
按照Step 1拆分的片段逻辑,直接使用数据库存储过程实现整个过程的统计分析,即写一个测试用的存储过程。
4. 执行测试存储过程和程序代码:
测试执行时,先执行被测功能代码逻辑,再基于同一个数据源执行存储过程的测试用例,最后对比测试表最终计算结果和被测功能代码逻辑的计算结果。
5.分析比较结果并定位问题:
如果结果不一致,可根据测试数据表存储的中间值,方便的进行手工核对,定位出错点是测试用例还是代码逻辑。
若用例出错,及时修正测试用例;若测试用例没有出错,则可以根据计算过程的中间值,结合debug工具,定位代码逻辑的bug点
好处:方便进行批量测试。
(三)基于探索式的随机测试方法
1. 基于结果表,从各种维度去查询数据,观察是否有可疑的数据
2. 基于结果表,用明显不合理的条件从反向去验证是否有异常数据存在
3. 基于源表,查询关键字段的数据状态。
观察是否有异常数据,对异常数据是否需要清洗机制。
4. 基于数据特点,分析是否可能有多对多的表关联情况,可能导致数据翻倍
需要分析出存储过程中涉及到哪些源表,源表之间的关系和数据特点
(四)基于生产模拟的用户验收式测试方法
从业务系统前台操作产生业务数据,通过真实模拟源数据的产生,检查结果表的统计数据是否正确。
最后,还是祝福我的小宝贝聪明勇敢,善良快乐。祝福我的家人们平平安安,健健康康,真的好爱你们。
版权声明:本文出自 暖洋洋 的51Testing软件测试博客:http://www.51testing.com/?15019819
原创作品,转载时请务必以超链接形式标明本文原始出处、作者信息和本声明,否则将追究法律责任。
webdriver提供的动作执行,如点击,有时会操作某些控件时,执行语句正常,但实际没有触发点击,原因很多,一般处理方式是使用它的execute_script方法注入js来执行(强化功能的一种方式), 但同样会存在这种情况,无法知道执行是否真的有效。
基于js中增加一个反馈机制,可以做到对执行效果有一定程序的控制,以click为例
return(function($,elem){ $(elem).one("click",function(){window._action_confirm=true; }); try{ var evt = document.createEvent( 'MouseEvents' ); evt.initEvent('click', true, true); elem.dispatchEvent(evt); }catch(e){ elem.click(); } var start_time = new Date().getTime(); while(true){ if(window._action_confirm){ return elem; } if((new Date().getTime())-start_time > 1500){ return null; } } })(jQuery,elem) |
执行的结果如果返回null,则表示js的click执行失败(可以再次尝试框架动作的执行),否则表示执行成功
版权声明:本文出自 xiadw 的51Testing软件测试博客:http://www.51testing.com/?215196
原创作品,转载时请务必以超链接形式标明本文原始出处、作者信息和本声明,否则将追究法律责任。