关于设计测试用例的方法,无论是在已经出版的专业测试书籍还是网络上的专业测试论坛中,都已经有了很多非常好的文章来专门讲解,笔者也不打算占用太多篇幅重新引用,大家如果有这方面需要,通过网络都可以很容易的找到这些资料。在这里,笔者只是想简单的评论一下很多初学者对这些方法容易产生的误解。
相信对于任何一个测试人员来说,等价类划分法和边界分析法都是最早接触,也是最基本、最容易使用的测试用例设计方法。很多朋友也都知道先使用等价类划分法划分出等价类,然后使用边界分析法确定测试需要的边界值。但是很多朋友也提到,在工作了一段时间后,发现这两中方法所能应用的地方越来越少,难道这两种方法真的只能应用在检查编辑框输入类型和输入长度的时候吗?当然不是。对于一些刚刚接触测试工作的朋友提出的这个问题,笔者认为现在市面上的很多测试书籍都要承担很大的责任。比如很多书中,在讲到测试用例设计时,都不约而同的使用同一个例子——Windows计算器程序。通常是告诉你对于计算器有一个允许的输入范围(比如允许输入一个大于0小于等于100的整数),然后要求设计相应的包含合法数据和不合法数据的测试用例。当然,仅仅是这样一条简单的描述,我们已经可以设计出很多测试用例和测试数据了,比如对于输入范围的考虑,对于输入数据类型的考虑,对于输入长度的考虑等等。但是在我们的实际工作中,很多时候看到的并不是这样过于简单的软件需求描述,很多时候这些内容都是隐含在一些算法或业务规则中的。
我们现在举个例子来看一下:
“双倍余额递减法是在不考虑固定资产残值的情况下,以平均年限法折旧率(不扣残值)的两倍作为折旧率,乘以每期期初固定资产折余价值求得每期折旧额的一种快速折旧的方法。
年折旧率=2÷预计使用年限×100%
月折旧率=年折旧率÷12
月折旧额=期初固定资产账面净值×月折旧率
为了保证固定资产使用年限终了时账面净值与预计净残值相等,在该固定资产折旧年限到期的前两年,将固定资产净值扣除预计净残值后的余额平均摊销。”
上面的内容是我国现行财务制度中关于固定资产折旧的一种方法——双倍余额递减法——的具体算法,大家可以在任何一本会计书中找到这部分内容以及一些相应的例子,不过我们这里并不关心固定资产的折旧到底是怎么一回事。笔者想知道的是大家在看完上面的描述后是否已经发现,我们在设计测试用例时,对于准备进行折旧的固定资产,至少应该包括预计折旧年限小于两年、等于两年和大于两年三种不同的类型。这也应该算是一个等价类划分法和边界分析法的应用吧。
实际上,制约我们更好的使用这些测试方法来进行测试用例设计的,并不是方法的应用范围不够宽广,而是因为如果我们不能对这些同实际业务相关的具体算法或业务规则进行深入的分析,就不可能挖掘出深层的测试需求,那么这两种方法的应用也就很有限了。这也是笔者在前面一再强调加深对软件需求了解的原因。
对于网络上也有讨论的另外两种方法——错误推测法和因果图法,则分别因为可靠性较差和操作相对复杂而并没有得到广泛的应用。
如何划分测试用例的粒度?
我们是不太可能在一个测试用例包含所有测试需求的,因为众多的功能以及不同的路径组合将使这样一个测试用例像巨无霸一般,完全不具有可操作性。——除非您的软件所包含的功能真的又少又简单,不过如果真的有这么一个软件,恐怕也没有测试和发布的必要了。
当然,这也并不是要您走向另一个极端,为需求中定义的每个特性或功能都提供一个甚至多个测试用例。这里的关键,是要寻找一个合适的度。
笔者推荐的方法是:关注有效功能。
有效功能:就是指在被测应用所涉及的实际业务中,当用户在手工状态下进行工作时,整个业务流程中对用户来说,具有实际意义那些功能。这个功能的特征是当我们把这个功能单独从计算机软件还原到用户的原始手工状态时,它的完成可以作为用户实际业务的一个阶段性结束的标志,而不是一旦从这个业务流程中独立出来就失去了意义。而该业务完成后,可以为其他用户或业务提供所需要的信息。
这里区分“有效功能”的关键有如下两个:
1. 这个功能是可以还原到用户原始的手工业务流程中去的。我们的计算机和软件,都是为了帮助用户解决手工业务中一些烦琐和低效的问题,而提出的一些忠实于原始工作方法或略有变通的解决方案,并不是要改变用户全部的业务流程。所以,应该从用户实际业务的角度来判断功能是否有效。
2. 这个功能是否可以标志着用户实际业务的一个阶段性结束?并且这项业务完成之后,被完成的业务实体是否可以交付给其他用户或业务以供完成下面的工作?
为了方便理解,我们可以先看一下下面的例子。
拿我们常见的财务软件来说,当添加一张会计凭证时,通常是需要填写会计科目,在使用计算机完成工作时,我们可以利用软件的功能,从很多备选科目中选择一个自己需要的科目,或者通过科目代码来输入科目。这项功能很有可能会作为一个特性要求出现在软件需求规格说明书中,那么这个科目的选择或输入是不是一个有效功能呢?让我们试着用上面规则来衡量一下。
首先,这个功能在用户手工业务处理过程中是存在的,不同的是这项功能是在用户填写凭证时,在自己的大脑中完成的——用户会根据需要,在自己记忆的科目中选择合适的填写上去,这项功能节省了用户在记忆大量会计科目时付出的额外劳动。我们可以认为这个功能是为用户原来的工作提供了一种简便的、变通的方法。
那么这项功能的完成对于用户来说意味着什么呢?我们从上面的描述中可以看到,用户希望软件提供的是可以添加一张完整的凭证这样的功能,而不仅仅是方便填写会计科目。填写会计科目只是用户在添加凭证时的一个步骤,单独把这个功能提取出来对用户来说没有任何实际意义。对于业务流程下游的用户,需要的也不仅仅只是一个会计科目的信息,而是一张包含了会计科目以及其他会计信息的完整的会计凭证,否则就无法进行下面的工作。这样看来,这个功能并不是一个有效的功能,我们可以把它最为需要测试的特性在测试需求中进行描述,却不应该作为一个单独的测试用例出现在我们的测试用例集中。而我们在测试用例中真正关注的,应该是添加会计凭证这个具有实际意义的功能。
另外,我们还需要关注如何将多个相互之间存在依赖关系的功能区分为单个的有效功能。例如,现在有A、B、C三个功能,其中B功能的开始必须依赖于A功能的完成,而且A功能如果出现不同的完成状态,B功能也会做出不同的反应;C功能对B功能的依赖也是如此。那么这时候,我们是否应当将三个相互依赖的功能包含在一个测试用例中呢?这样的做法也不是不可以,但是我们也可以先判断一下,这三个功能是否都是有效功能(您可以使用前面提到的方法来试着评判一下)?如果A、B、C都是独立的有效功能,那么我们可以从上面的描述中发现,它们之间存在的依赖性,可以理解为是一种状态或者说数据的依赖性。后一个功能关心的,是前一个功能最终提供给它的是什么样的“输入”,而不是前一个功能到底作了些什么。这样看来,我们完全可以将它们分别包含在几个独立的测试用例中,而在每个测试用例的开始,把不同的输入作为不同前置条件来描述。
测试用例是否应该包含所有的细节?
这是在网络上听到诉苦最多的又一个热点问题:公司要求按照一个严格的规范来开展测试工作,必须书写所有的测试用例文档,要求文档的书写一定要具体,并在执行测试时要参考测试用例文档来进行。如果测试用例文档写的过于简略,则会被领导斥为偷懒。但是,如果文档中的对操作步骤描述的过于具体,像下面这样:
01. 在“用户名”项中输入“user”;
02. 在“密码”项中输入“password”;
03. 点击“确定”按钮。
这样的描述方式表面看起来可操作性似乎是增强了,任何人拿到这个文档都可以充当测试人员,检查一下这个功能是否存在缺陷。但是我们不要忘记,在开发过程中,变更的存在是必然的。一旦需求、设计或者应用程序中的某些细节发生了变化,比如“用户名”变成了“操作员”,“密码”变成了“口令”,“确定”变成了“登录”,或者输入项所接受的数据类型发生了变化,那么同这部分内容相关的所有的测试用例都需要修改。否则,我们凭什么可以保证这些描述不同的测试用例说的是同一样东西?如果我们只有这么一个测试用例,也许一切都不是问题,但是对于一个业务复杂、功能完整的系统,如果按照这样的方法描述测试用例,最终要么产生大量的测试用例文档,要么产生过长的单个文档。无论是那一种,如果发生了类似于上面说的变化,维护文档都将变成一次地狱式的体验。
这也是为什么在网络上频频出现的这个问题,而且每次出现都会受到测试同行们的关注的原因。那么这个问题应该任何解决呢?测试用例是不是把所有的流程写出来就可以了呢?如何在减少测试用例文档中包含过多琐碎的细节的同时保证测试用例的可操作性呢?又有什么方法可以提高我们维护测试用例文档的效率呢?
笔者的建议是:关注“测试思想”而不是关注操作步骤。
要理解这个问题,首先要弄明白测试用例的作用。
就像用例一样,测试用例并不是用来描述具体的实现的,而是着重描述处理问题的思路——对于一项明确的测试内容进行测试的思路。作为测试用例的设计人员,如何理解基本流和备选流?如何深入分析并找到所有需要覆盖的路径和需要检查的特性?我们在测试用例中应该用容易理解的自然语言清晰的来描述我们将要如何进行测试,而不是简单的把在应用程序上如何操作的烦琐的步骤记录下来。把测试用例设计当成填写具体操作步骤的表格是人们对测试用例最大的误解。
传统的测试用例文档编写有两种方式。
一种是填写操作步骤列表:将在软件上进行的操作步骤一步一步详细的记录下来,包括所有被操作的项目和相应的值。
另一种是填写测试矩阵:将被操作项作为矩阵中的一个字段,而矩阵中的一条条记录,则是这些字段的值。
网络上对于这两种方式孰优孰劣的争论,将大家错误的引导向了两个极端:要么采用A,要么采用B。而大家却忽视了一点,对于工作方法的争论,本质上同工具的争论并不是一回事(例如曾经的VC、BCB之挣)。如果不同的方法各有优势,我们完全可以通过变通的方法,把优势的部分组合在一起来使用。
操作步骤列表的优势在于对基本流和备选流进行分析后,它可以清晰的描述您的测试思路。而测试矩阵则更适合于用来存放测试数据,特别是那些需要人工赋予一个确定的值的特性。
下面,我们来看一个例子,当然,这个例子同样是杜撰的。
需求名称:用户登录安全验证
需求描述:用户登录安全验证是为了保证所有登录到系统中的用户,都是由系统管理员预先在系统中设定的。使用系统中不存在的用户名,或者用户名输入正确,但密码输入错误情况,都无法登录到系统中。当用户使用了不存在的用户名或错误的密码时,系统应分别给出适当的提示。如果用户连续三次无法使用正确的用户名和密码登录到系统,则系统应给出适当的提示,并退出当前程序。如果用户使用正确的用户名和密码登录到系统,则退出界面,转到系统主界面。对于用户登录界面和程序主界面,请参考相应的UI设计文档。
测试需求:
01. 检查能否使用正确的用户名和密码登录到系统;
02. 检查能否使用错误的用户名或密码登录到系统;
03. 检查使用错误的用户名和密码登录失败超过三次,是否会自动退出当前程序。
测试用例:
序号
操作过程描述
1
输入用户名。
2 输入密码。
3 确认登录。
序号 用户名 密码 预期结果
1 正确的用户名 正确的密码 登录到系统并转到系统主界面
2 正确的用户名 错误的密码 无法登录到系统并提示密码错误
3 错误的用户名 正确的密码 无法登录到系统并提示用户名错误
4 错误的用户名 错误的密码 无法登录到系统并退出当前程序
5 空用户名 …… ……
这个例子并不是按照RUP提供的标准模板编写的,它的目的只是要为大家展示,用前面所讲的方法,整理出来的测试需求和设计完成的测试用例到底是个什么样子。所以省略了很多细节,不过大家在实际编写测试用例文档的时候,可以根据自己的需要把相应的内容添加上去。同时,也可以在用户名和密码两个字段中填写准备使用的具体数据。
相信大家已经看到,在我们的例子中,测试需求同测试用例之间并非是一一对应的关系因为一条测试需求未必是明确的对应到一个有效功能的(其实测试需求本身同软件需求和用例之间也未必是一一对应的)。而我们的测试用例所关注的,应该是一个有效功能。不过您不用担心,这种情况并不会增加您管理测试需求和测试用例的成本,现在市面上提供的测试工具中已经有些是专门用来维护软件需求、测试需求同测试用例之间的关系的,并且它们提供的强大的视图功能还可以让您很容易的查看到测试用例对测试需求的覆盖情况。
对于例子中的测试用例文档,已经被分成了两个部分,一部分是描述了测试用例执行者所应遵循的操作过程,一部分则是在操作中需要使用到的测试数据。这样做的原因是在我们的实际工作中,尤其是在进行功能测试时,很多时候都是使用雷同的操作过程和不同的测试数据来进行的。而使用上面的方法,可以不用再对原本在多个用例中重复出现的操作过程再次描述,而可以把更多的精力放到测试数据的设计和准备上。这样作带来的副产品,就是提高了测试用例的可维护性。
怎么?还有人对笔者的观点持怀疑态度?那好吧,那么我们来尝试着证明一下。
我们的邮递员要在5个城市内奔波,并且每个城市都有些邮件需要投递,他需要找到可以一次走遍5个城市的所有路线。这听起来似乎并不是太复杂,利用我们已有的数学知识,很容易就可以得到答案。但是对于我们要测试的内容,通常都会包含更多复杂的规则。
例如,我们有三个文本框,每个文本框每次都只能输入一个英文大写字母,允许输入的值只包括:A、B、C三个字母,当三个文本框输入不同的值的时候,我们不知道会发生什么,也不知道它们之间是否会互相影响,所以需要您来设计可以覆盖所有输入情况的测试用例来测试它。
瞧,这很简单不是吗?如果我们希望每个测试用例在执行时一旦出现缺陷都可以很快的找到导致缺陷的原因,那么最好的办法就是每个测试用例只包括一个同其他测试用例不同的输入值。那么可供我们输入的值都有哪些呢?嗯,对于每个文本框,都至少有A、B、C三种已经声明的不同的值,另外,我们还要考虑当文本框为空、输入空格、输入非英文字符以及输入A、B、C之外的英文字符的情况。那么按照上面的方法,我们会有多少测试用例呢?答案是343个。这只是一个很简单的例子,我们工作中遇到的软件的业务规则和特性决不会比这少的,那会有多少个测试用例呢?God knows. 不过至少有一点可以肯定,我们无法在原有业务规则发生变化时高效的、无差错的维护完343个测试用例。
但是如果使用我们前面所描述的方法,对于操作过程的改变,您只需要重新维护一次,而对测试数据的维护,也同样是非常简单的,而且并不会因为连续大量修改时出现的错误导致测试用例本身出现歧意。
在将主要精力从“设计”操作步骤转移到设计测试数据之后,我们还将从这种方法中获得更大的益处——通过“逆向测试数据分析”来提高测试工作的整体效率。
这种“逆向测试数据分析”的方法,是假设软件中存在多个互相依赖的功能,而且这些功能中包含在“依赖链”最末端,并且不再被其他功能依赖的功能。
在我们准备测试数据时,首先从这个“依赖链”最末端的功能开始,分析这个功能会对不同的输入产生哪些不同的结果。然后将所有的输入进行整理,分清哪些输入是来源于其前一个功能的输出。之后,对该功能的上一个功能进行同样的分析,整理出所有的输入和输出,但是这个功能的输出至少应该包含“依赖链”最末端功能接收到的全部输入。
继续按照这样的思路循环向上,直到到达“依赖链”开始端的功能。
不知道您在工作中有没有遇到这样的情况:在对那些“依赖链”上的功能进行测试时,一开始并没有严格的规范测试数据的使用,结果前一个功能测试时产生的数据根本无法在下面的工作中被很好的利用起来,反而因为大量无效数据增加了后面功能的测试难度。最终不得不对每一个功能重新准备测试数据。大量的时间,浪费在了这些重复而低效的劳动上。
当然,如果希望可以进一步提高某个阶段测试工作的效率,还可以考虑应用“设计测试过程”的方法。这里所说的测试过程,指的是我们在执行测试时所设定的执行测试用例的先后顺序。之所以这样作,是因为可以充分的利用不同功能之间的耦合性,尽量通过一次操作来检查尽量多的内容,从而降低已完成工作的无效性或低效性,最终提高某个阶段的整体工作效率。不过,这项工作同样要求操作者必须对被测的系统所涉及的所有业务以及这些业务之间的关系都非常熟悉才行。
如果您正被测试工作的低效问题所困扰,那么建议您尝试一下上面的方法,希望会对您的工作有所帮助。
如何评价测试用例的好坏?
这部分内容的出现,完全是因为不久前同几个朋友的一次讨论。当时大家都认为对于一个测试用例好坏的评价,无外乎两个标准:是否可以发现尚未发现的软件缺陷?是否可以覆盖全部的测试需求?但是后来发现这两个标准对于一些问题是处理不了的。例如,对于一个质量非常好的软件产品,存在的软件缺陷异乎寻常的少,测试用例设计人员准备了大量的测试用例,已经完全覆盖了测试需求,但是只有很少一部分测试用例在执行时发现了缺陷,而其他用例都顺利通过了。那么是否就可以认为顺利通过的那部分测试用例不好呢?
对于这个问题,笔者认为不管是测试用例是否可以发现尚未发现的缺陷,还是测试用例对测试需求的覆盖度,都是用来评估测试设计人员工作能力和经验的标准,而对于如何评价测试用例的优劣,应该还有其他标准。当然,在不同的团队中可能存在不同的标准,但下面两条应该是适合于任何团队的。
1.易用性。对于一个即熟悉测试工作,又熟悉被测应用的测试人员,应当可以花费
很少的时间就可以理解测试用例中表达的测试思路,并可以很快的执行完这个测试用例。
2.易维护性。当开发过程中的某些因素影响了测试需求,测试用例的作者或其他测试设
计人员,应该可以花费很少的时间就完成定位并维护所有相关测试用例的工作。
一些题外话
曾经有朋友问:如果测试用例同软件的具体实现互相独立,怎么保证测试用例的可操作性呢?怎么保证任何一个人都可以一拿到测试用例就可以直接上机执行测试呢?笔者的回答是:尽量不要让这种事情发生。
首先,笔者一直不赞同那种让“任何人”都可以充当测试人员,按照手中测试用例执行测试的做法。因为对于被测应用的全面了解和熟悉,是作为一个测试人员最基本的要求——在前面的文字中对于软件需求的熟悉也多次被强调。我们无法预知让一个对软件以及软件所涉及的实际业务不够熟悉的人,依靠一份他同样不熟悉的操作步骤列表来执行测试会有什么样的结果。但是有一点可以明确,这就好像让一个没有医学背景的人,仅仅依靠一本七年制本硕连读的《内科学》教材来为患者检查是否患有心脏病,最终结果是患者承担了全部的风险。
其次,测试用例的本来目的是为了描述我们的“测试思想”——也就是“怎样测”,而是否可以熟练的操作被测应用,并不是在测试用例这个“对象”中所要考虑和保证的,应当剥离出来,作为独立的部分进行处理。而问题的关键,在于如何保证拿到测试用例的人有足够的能力和经验来执行测试,这完全可以通过公司内部对软件及软件所涉及的实际业务的培训,或者软件的操作手册。总之,还是尽量不要随意的加入“任何人”到测试工作中吧。
作者简介
网络ID :jackei,长期活跃于“测试时代”、“中国软件测试社区”、“51testing”、“51CMM”、“CSDN”等测试网站和测试论坛。《测试时代期刊》杂志责任编辑及撰稿人。
学院派测试人员,坚信“实践是检验真理的唯一标准”。反对学术腐败、造假。坚信做一个好的测试人员,首先要为人正直、诚实、严谨、耐心、乐于助人。现供职于广州某软件公司,从事软件测试工作,致力于测试过程改进和测试基础理论方面的研究。
对于本文中已经包含的以及还未包含的内容,如果需要讨论,您都可以使用E-Mail:jackei_chan@hotmail.com 同笔者联系。