文章内容参考了腾讯《不测的秘密》以及个人的一些见解,有兴趣的同学可以一起交流
第一式:差异化(靠人)
寻找最小的精准测试对象-迭代之间的差异部分,差异部分所影响的其他功能;
明确被测对象,关注代码实现,从代码层面明确测试范围
需求差异:
能够清晰的分析出来产品迭代过程中,产品的差异化,以及差异产生后会影响原有什么功能。
需求差异分析方法:
功能流程图:图形表示功能和功能之间的关系以及功能走向关系
数据流向图(DFD):从数据传递和加工角度,图形化表示系统的功能逻辑、数据在系统内部的逻辑流向和逻辑变换过程
状态变迁图(STD):指明外部事件的结果系统将如何动作(eg:财务对账状态)
技术实现差异:
产品开发过程中,修改不同模块和源码,不同的实现方式等等,会在技术层面影响到其他看起来不相关的功能。
开发框架变更、修改多模块调用的公共代码、应用架构的重构、数据库结构变化、数据库类型切换
系统设计上差异:
从系统设计上丽姐技术实现上的差异,搞清楚应用系统的架构关系,就可比较容易的知道局部发送变化时,对整体产品架构的影响
例如,图3-4是一个对账系统的应用系统架构。观察这个系统架构,例如“账单核心”系统发生变更时,就会影响所有的外围系统。而“产品层”发生变更,实际上只需要关注它和“账单核心”系统的交互是否还能正常进行即可。
逐步分析差异
对于单个模块的实现细节,需要逐步分析差异:
时序图:描述对象之间发送消息的时间顺序,显示多个对象之间的动态协作关系
这个时序图清晰地描述了对账流水拉取的接口是如何在各个系统间进行交互的。如果某个迭代或需求变化时,将分页拉取改为单次全部拉取,那么我们就可以清晰地分析出这个差异部分是在时序图中的右半侧,影响的是bizrecon 系统和ccrprod系统,对步骤1和schedule系统则没有任何影响。
工程上的差异:
实现代码—编译器编译可执行文件
编译时,两个因素会影响实际编译结果,一个是引用的第三方库;二是编译配置的不同的编译参数、包括不同的编译开关。
实用分析方法
SVN-diff∶
代码管理工具SVN,自带了版本对比命令,可以对比任何两个SVN文件或版本,标识出具体的差异化源码。此外,一些开源的工具,如commit-monitor,也能实时的监控SVN的递交并对比递交前后的差异。
GIT:分支代码对比
文件对比方法:
编译后的文件对比是相对比较困难的,即使是完全相同的源码,两次编译的结果,我们也很难直接确定它们是否存在差异。要真正对比两个编译后文件是否一致,基于反汇编的基本块跳转关系的二进制对比,是比较准确的一种二进制对比技术。
腾讯:IDA pro-自动分析二进制文件差异化
第二式:技术治理(靠团队)
耦合:
1)内容耦合∶如果发生下列情形,两个模块之间就发生了内容耦合∶
一个模块直接访问另一个模块的内部数据;
一个模块不通过正常入口转到另一模块内部;
两个模块有一部分程序代码重叠(只可能出现在汇编语言中);
一个模块有多个入口。
2)公共耦合∶若一组模块都访问同一个公共数据环境,则它们之间的耦合就称为公共耦合。
公共的数据环境可以是全局数据结构、共享的通信区、内存的公共覆盖区等。
3)外部耦合∶一组模块都访问同一全局简单变量而不是同一全局数据结构,而且不是通过参数表传递该全局变量的信息,则称之为外部耦合。
4)控制耦合∶如果一个模块通过传送开关、标志、名字等控制信息,明显地控制选择另一模块的功能,就是控制耦合。
5)标记耦合∶一组模块通过参数表传递记录信息,就是标记耦合。这个记录是某一数据结构的子结构,而不是简单变量。
6)数据耦合∶一个模块访问另一个模块时,彼此之间是通过简单数据参数(不是控制参数、公共数据结构或外部变量)来交换输入、输出信息的。
7)非直接耦合∶两个模块之间没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的。
当然这个耦合的分类是比较学术和严谨的,但在实际互联网产品开发中,常见的耦合并没有这么多,常见并且通常需要考虑的耦合类型有∶
数据库/文件/缓存区耦合;
同步耦合,如函数/方法/类的直接调用,WebService、EJB,或分布式事务;
异步耦合,如异步消息,短连接请求等。
在实际测试工作中,梳理清楚这些耦合是非常有必要的
技术治理:
1、系统内治理:
系统内的耦合类型最主要的类型有∶直接调用、数据共用(内存/数据库/文件)和传递等。
直接调用是最常见的一种耦合类型。常见的IDE提供了调用关系的跟进方法(例如在Eclipse中,可以在F4的Hierarchy View中查看接口和类之间的关系)。当然这只能看少数几个类或方法。批量治理的方法有吗我们推荐三种方法,可以对付常见的直接调用,如下所示∶
最简单的是用关键词索引法。我们先用class,function等关键词,一次性提取出所有的类、函数/方法,然后用每个类、函数/方法作为关键词,在全量代码中去搜索,看它们在什么地方出现,我们就知道这些对象在什么地方被调用了。我们通过设计合理的数据记录模型,再反向进行拼接,就可以知道调用关系的全图。这种方法虽然能帮助我们快速找到调用对象,但不一定完全准确,我们一定会找到比实际更多的对象。例如重名就是一个很重要的干扰因素。
针对需要编译的高级语言领域,如C、C++,利用分析二进制的逆向方法,来动态解析调用关系。例如,利用IDA的逆向函数来对二进制文件进行分析,可以找到互相之间的调用关系。
动态解析的方法:区别是针对JAVA领域,通过Java自带的方法,在Java虚拟机中,对字节码进行增强,通过对Java程序运行过程中的调用路径进行记录,从而跟踪到类/方法之间的调用关系。
2、系统间治理:
间的耦合主要有:
基于消息的异步调用或回调。这里包括各种各样的消息类型,如MQ(B/S),管道(Windows客户端)等。
通过WebService、EJB等同步调用。
最有效的研习方法是跟开发一起“双修”。
以B/S系统为例,我们需要一个有效的服务治理方法来管理服务之间的调用关系。这个方面,行业做得比较好的是阿里巴巴的分布式服务框架Dubbo。Dubbo其实也不是一个专门用来做服务治理的框架,它是一个比较强大的高性能和透明化的RPC远程服务调用方案。在这里我们仅提到服务治理部分,是为了用它作为一个例子来说明B/S系统间服务治理的一种良好解决方案。
Dubbo通过控制系统间服务调用的权限(服务需要注册后才能对外发布服务,同样服务使用方也要先跟Dubbo申请权限后方可调用对应的服务),来完成对所有暴露外部服务和调用方的管理。对开发和测试来说,有了这个就可以画出系统间服务调用的全图,从而可以轻而易举地去判断服务之间的影响关系了。
对于客户端的系统(模块)之间的通信,同样适用于这种方法。例如通过建立消息中心来管理所有模块的IPC通信,也是一种良好的管理系统(模块)间关系链的方法。
在一些测试的版本中加入一些监控,也是一种帮助杂乱无章的系统耦合进行梳理的思路。例如在建立连接、会话的过程中,或者在操作系统中监控特定范围内的跨进程访问记录,再通过这些记录进一步整理出存在相互之间调用的可能性。为什么不试一试呢?
即使以上说的方法在实施的过程中都有困难,还有最后一步,人工梳理,笨是笨了点,但梳理工作本身也是测试人员系统地去认识系统的过程,这样的过程也会极大地帮助测试人员更理解应用架构,以及系统的各种实现细节。
3、数据库治理
不管是B/S系统还是C/S系统,关系型数据库都是比较常见的(移动客户端和Windows客户端除外)。
在老旧的Sever里,对关系型数据库的依赖是非常大的,有些甚至到了令人发指的程度。除了表与表之间的直接关联,还有各种存储过程对数据表、字段的引用,存储过程的互相调用,更有触发器这样的隐形侠神出鬼没。
从治标要治本的角度去看,错综复杂的数据库对象之间的关联是不太好的一种应用架构方式。关系型数据库尽管非常强大,也能承担起一些应用逻辑,但如果把太多的业务逻辑都放在关系型数据库里,是不太方便后期的跟踪和维护的。数据库的任何读写也会伴随着大量的I/O产生,对性能也会带来很明显的瓶颈。
数据库还是尽可能地回归到数据存储和读写这个根本的定位上来,而把更多的业务处理放到应用服务器中去。因此,如果在实际的项目中存在关系型数据库内部的大量耦合,是一定要首先考虑督促项目团队去朝以上方向努力的。
如果实在没有这样的机会。考虑一下关键词索引,准确度稍微低了些,但是简单、粗暴、有效,能解决80%以上的问题。
4.函数调用链获取Doxygen(逆向二进制获取函数调用关系)(测开技术方案)
第三式:度量及分析闭环(靠流程)
测试精准度怎么衡量,怎么分析?
代码覆盖率:(增量代码覆盖率比较有参考价值)
1.语句覆盖
语句覆盖(Statement Coverage)又叫行覆盖,是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否执行到了。让我们看一段代码:
int foo(int a, int b)
{
return a / b;
}
如果我们设计了这样一组用例:
TC: a=1, b=3
很明显,语句覆盖率达到了100%。但是,聪明的你一定想到了,语句覆盖率虽然达到了100%,却没有发现最简单的bug∶ b=0时,会抛出一个除零异常。好吧,看来这个语句覆盖不是很靠谱。语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等。
2.判定覆盖
判定覆盖(Decision Coverage)度量程序中每一个判定的分支是否都被测试到了。废话不说,直接上代码:
int foo(int a, int b)
{
if (a < 10 || b < 10) // 判定
{
return 0; // 分支一
}
else
{
return 1; // 分支二
}
}
设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,设计如下的案例就能达到判定覆盖率100%:
TestCaes1: a = 5, b = 任意数字 覆盖了分支一
TestCaes2: a = 15, b = 15 覆盖了分支二
3.条件覆盖
条件覆盖(Condition Coverage)度量判定中的每个子表达式结果true和false是否被测试到了,例如:
int foo(int a, int b)
{
if (a < 10 || b < 10) // 判定
{
return 0; // 分支一
}
else
{
return 1; // 分支二
}
}
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,设计了如下的案例:
TestCase1: a = 5, b = 5 true, true
TestCase4: a = 15, b = 15 false, false
通过上面的例子,我们应该很清楚判定覆盖和条件覆盖的区别。需要特别注意的是:条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false都测试到了就行了。因此,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。比如上面的例子,假如设计的案例为:
TestCase1: a = 5, b = 15 true, false 分支一
TestCase1: a = 15, b = 5 false, true 分支一
我们看到,虽然我们完整地做到了条件覆盖,但是却没有做到完整的判定覆盖,只覆盖了分支一。从上面的例子也可以看出,这两种覆盖方式看起来似乎都不如听上去那么完美
4.路径覆盖
路径覆盖(Path Coverage)度量函数的每一个分支是否都被执行到。这句话也非常好理解,就是所有可能的分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试路径会随着分支的数量指数级别地增加。比如下面的测试代码中有两个判定分支:
int foo(int a, int b)
{
int nReturn = 0;
if (a < 10)
{// 分支一
nReturn += 1;
}
if (b < 10)
{// 分支二
nReturn += 10;
}
return nReturn;
}
被测代码中nReturn的结果一共有四种可能的返回值:0,1,10,11,而上面针对每种覆盖率设计的测试案例只覆盖了部分返回值,因此,可以说使用上面任一种覆盖方式,虽然覆盖率达到了100%,但是并没有测试完全。接下来我们看看针对路径覆盖设计出来的测试案例:
TestCase1 a = 5, b = 5 nReturn = 0
TestCase2 a = 15, b = 5 nReturn = 1
TestCase3 a = 5, b = 15 nReturn = 10
TestCase4 a = 15, b = 15 nReturn = 11
路径覆盖率100%。太棒了!路径覆盖将所有可能的返回值都测试到了。这也正是它被很多人认为是“最强覆盖”的原因。
代码覆盖率测量主要有以下三种方式:
1.源代码检测
将检测语句添加到源代码中,并使用正常的编译工具链编译代码以生成检测的程序集。这是我们常说的插桩,Gcov 、GCT
2. 中间代码插桩
这种方法在代码执行时从运行时环境收集信息以确定覆盖率信息。JaCoCo 和 Coverage 、Emma
3. 二进制代码插桩
对程序真正运行的目标文件进行探针插入操作。XDebug
分析步骤:
- 测试分析:通过差异化的测试分析得到测试范围集合。 2)测试执行:手工执行用例。 3)代码覆盖率统计:工具自动收集。 4)覆盖率结果分析:需人工分析。 5)反馈调整:根据分析的结果,对于应覆盖但未覆盖到的代码,需要补充用例;对于无需覆盖的代码,记录下来,为下次测试分析提供参考
代码覆盖率结果分析参考模式-代码覆盖率可不覆盖的
1.try catch类
示例:
try{
//代码区
}catch(Exception e){
//异常处理
e.printStackTrace();
}
当程序运行遇到异常时,try catch类才会进入catch的异常处理模块,异常处理模块一般是调整SDK通用的函数,不会出现问题,因此不需要特别构造异常条件来覆盖此类代码,运行try的程序代码模块即可,风险可控。
2.日志类
示例:
switch(i)
{
case NUM1:
System.out.println(1);
break;
case NUM2:
System.out.println(2);
break;
default:
System.out.println("default");
break;
}
如上所示,日志类代码一般是调系统API输出信息日志,不会出问题,不需要所有的日志代码均运行过。此类代码选择性执行部分路径即可,风险可控。
3.空指针判断
if (! m_bInit)
{
return FALSE;
}
如上所示,m_bInit是一个指针,这段代码是当该指针为空的一个异常处理逻辑,异常处理的内容也比较简单,就是直接返回,指针为空的逻辑非白盒测试很难进入,故在只有黑盒测试用例的时候,可以不覆盖,风险可控。
4.其他风险可控的不覆盖类型
小宇在实践中还积累了下面几个不覆盖也风险可控的代码类型:
□ 预埋逻辑:本次没有任何功能表现,只是为了长远考虑预埋的代码逻辑,这部分代码黑盒覆盖不到,对本次业务发布质量无影响,故风险可控;
□ 冗余代码:这部分是历史遗留的冗余代码,没有任何功能可以进入,也可以不覆盖;
□ 尚未使用的公共库代码:这部分代码也是从全局考虑预设的代码,有可能很快被其他人使用,但对本次业务发布质量影响不大,可以考虑使用接口测试等手段来覆盖验证,不影响本次业务发布。
第四式:知识库(靠积累)
用例预分析-代码发生变更,机器自动测试分析,并推荐测试用例集
1、原理使用代码覆盖率
2、采集用例执行时,用例和代码底层函数调用关系(从函数角度出发,函数只关联一个最精简的测试用例集-对函数路径分支进行标记)
3、落库保存函数与用例关联关系
第五式:用例预分析(靠创新、靠技术)
1、变更分析
SVN diff与二进制变更的结合处才是代码发生变化的真正地方,结合处两侧的情况是这样的∶
1)SVN未被包含的部分是无效代码的变更,因为未引起二进制的变化。
2)二进制变更未包含的部分是无效的二进制代码变化,因为对应的源码并未发生变化。
通过上面的方式我们就可以精准获得变更的函数,即真正发生变化的地方。
2、由变更函数到变更用例
当我们完成了对变更的分析之后,就会得到变更函数集合。我们就可以拿着这个变更函数的集合到用例知识库中查找对应的用例集合。我们可以通过用例关联函数过滤映射中的字段function_key查找到对应的用例ID,再拿着用例ID到用例详细信息表中查询用例的详情。
3、变更接口和边界耦合关系
在系统中存在一些接口调用,这些接口可能是开发专门编写的一些公共接口供各项目调用,也可能是各项目之间的一些耦合接口。一般当公共接口变更时可能对各项目都有影响,而项目之间的耦合接口的变更也可能对相关项目造成影响。所以当接口发生变更时系统需要识别并且通知到测试,对于已经自动化脚本实现的接口可以直接通知脚本运行。
第六式:用例预分析
对应函数功能实现自动化用例
第七式:质量评估
产品发布需要满足质量标准,才可以发布,有以下几个指标:
增量代码覆盖率需要达到90%以上;严重Bug需要全部修复完毕;
挂起Bug比率需要控制在5%以内;
产品的性能、稳定性测试通过。
除此之外,我们不仅要关注质量结果,也要关注研发过程,打造有战斗力的研发团队,项目的过程指标也要达到标准∶
测试任务全部按照计划执行完;
测试计划实际投入与预期符合;
项目的千行Bug率要控制在3个以内;
Bug发现率在提测周期应该呈收敛趋势。
对测试保障的划分:
版权归原作者 jayce_qian 所有, 如有侵权,请联系我们删除。