笔记_测试的艺术

测试的艺术 The Art of Software Testing


软件测试是一项技术性工作,但同时也涉及经济学和人类心理学的一些重要因素。

大多数情况下, 我们即使想对所有内容进行测试, 也是不可能完成的. 即使一个看起来非常简单的程序,其可能的输入与输出组合可达到数百种甚至数千种,对所有的可能情况都设计测试用例是不切合实际的。对一
个复杂的应用程序进行完全的测试,将耗费大最的时间和人力资源,以至于在经济上是不可行的。

要成功地测试一个软件应用程序,测试人员也需要有正确的态度(愿景vision)。在某些情况下,测试人员的态度可能比实际的测试过程本身还要重要。


一、测试的定义:

存在的误区:

• “软件测试就是证明软件不存在错误的过程。”
• “软件测试的目的在于证明软件能够正确完成其预定的功能。”
• “软件测试就是建立一个‘软件做了其应该做的’信心的过程。”

更适合的定义:

“测试是为发现错误而执行程序的过程”。

如果我们的目的是证明程序中不存在错误,那就会在潜意识中倾向于实现这个目标,也就是说,我们会倾向于选择可能较少导致程序失效的测试数据。另一方面,如果我们的目标在于证明程序中存在错误,我们设计的测试数据就有可能更多地发现间题。


二、什么是成功的测试

误区:

将没发现错误的测试用例称为一次“成功的测试”,而将发现了某个新错误的测试称为“不成功的测试”。

正确的是:

如果在测试某段程序时发现了错误,而且这些错误是可以修复的,就将这次合理设计并得到有效执行的测试称作是“成功的”。如果本次测试可以最终确定再无其他可查出的错误,同样也被称作是“成功的”。所谓“不成功的”测试,仅指未能适当地对程序进行检查,在大多数情况下,未能找出错误的测试被认为是“不成功的”,这是因为认为软件中不包含错误的观点基本上是不切实际的.

能发现新错误的测试用例不太可能被认为是“不成功的”;相反,能发现错误就证明它是值得设计的。一个“不成功的”测试用例, 会使程序输出正确的结果,但不能发现任何错误。

软件测试更适合被看作是试图发现程序中错误(假设其存在)的破坏性的过程。一个成功的测试用例,通过诱发程序发生错误,可以在这个方向上促进软件质量的改进。当然,最终我们还是要通过软件测试来建立某种程度的信心:软件做了其应该做的,未做其不应该做的。但是通过对错误的不断研究是实现这个目的的最佳途径.


三、软件测试的经济学

应对测试经济学的挑战,在开始测试之前建立某些策略。黑盒测试和白盒测试是两种最普遍的策略。

黑盒测试

黑盒测试是一种重要的测试策略,又称为数据驱动的测试或输入/输出驱动的测试。使用这种测试方法时,将程序视为一个黑盒子。测试目标与程序的内部机制和结构完全无关,而是将重点集中放在发现程序不按其规范正确运行的环境条件, 不需要去了解程序的内部结构。

穷举输入测试是无法实现的,这有两方面的含义,一是我们无法测试一个程序以确保它是无错的,二是软件测试中需要考虑的一个基本问题是软件测试的经济学。也就是说,由于穷举测试是不可能的,测试投人的目标在于通过有限的测试用例,最大限度地提高发现的问题的数量,以取得最好的测试效果。

白盒测试

另一种测试策略称为白盒测试或称逻辑驱动的测试,允许我们检查程序的内部结构。这种测试策略对程序的逻辑结构迸行检查,从中获取测试数据(遗憾的是,常常忽略了程序的规范)。

穷举路径测试也决不能保证程序符合其设计规范, 也不能发现缺少了哪些必需路径, 而且可能不会暴露数据敏感错误。

尽管穷举输入测试要强于穷举路径测试,但两者都不是有效的方法,因为这两种方法都不可行。


四、软件测试的原则
  1. 测试用例中一个必需部分是对预期输出或结果进行定义
  2. 程序员应避免测试自己编写的程序
  3. 编写软件的组织不应当测试自已编写的软件
  4. 应当彻底检查每个测试的执行结果
  5. 测试用例的编写不仅应当根据有效和预料到的输入情况,而且也应当根据无效和未预料到的输入情况
  6. 检查程序是否“未做其应该做的”仅是测试的一半,测试的另一半是检查程是否“做了其不应该做的”
  7. 应避免测试用例用后即弃,除非软件本身就是个一次性的软件
  8. 计划测试工作时不应默许假定不会发现错误
  9. 程序某部分存在更多错误的可能性,与该部分已发现错误的数量成正比(错误总是倾向于聚集存在)
  10. 软件测试是一项极富创造性,极具智力的挑战性的工作

如果对程序的更改导致了程序某个先前可以执行的部分发生了故障,这个故障往往是不会被发现的,保留测试用例,当程序其他部件发生更动后重新执行,这就是我们所谓的“回归测试”。

三个重要的测试原则:

• 软件测试是为发现错误而执行程序的过程。
• 一个好的测试用例具有较高的发现某个尚未发现的错误的可能性。
• 一个成功的测试用例能够发现某个尚未发现的错误。


五、代码检查与评审

在代码走查中,一组开发人员(三至四人为最佳)对代码进行审核。参加者当中只有一人是程序编写者。因此,软件测试的主要工作是由其他人,而不是软件编写者本人来完成。这符合“软件编写者往往不能有效地测试自己编写的软件”的测试原则。代码检查走查与基于计算机的测试是互补的。缺少其中任何一种,错误检查的效率都会降低。

在检查进行时,主要进行两项活动:

  1. 由程序编码人员逐条语句讲述程序的逻辑结构。在讲述的过程当中,小组的其他成员应提问题、判断是否存在错误。在讲述中,很可能是程序编码人员本人而不是其他小组成员发现了大部分错误。换句话说,对着大家大声朗读程序,这种简单的做法看来是一个非常有效的错误检查方法。
  2. 对着历来常见的编码错误列表分析程序。协调人负责确保检查会议的讨论高效地进行、每个参与者都将注意力集中于查找错误而不是修正错误(错误的修正由程序员在检查会议之后完成)。

要使检查过程有成效,必须树立正确的态度。如果程序员将代码检查视为对其人格的攻击、采取了防范的态度,那么检查过程就不会有效果。正确的做法是,程序员必须怀着非自我本位的态度来对待检查过程,对整个过程采取积极和建设性的态度:代码检查的目标是发现程序中的错误,从而改进软件的质量。

除了可以发现错误这个主要作用之外,代码检查还有几个有益的附带作用。其一,程序员通常会得到编程风格、算法选择及编程技术等方面的反馈信息。其他参与者也可以通过接触其他程序员的错误和编程风格而同样受益匪浅。还有,代码检查还是早期发现程序中最易出错部分的方法之一,有助于在基于计算机的测试过程中将更多的注意力集中在这些地方。

代码检查过程的一个重要部分就是对照一份错误列表,来检查程序是否存在常见错误。遗憾的是,有些错误列表更多地注重编程风格而不是错误。错误检查太过模糊而实际上没有用。而错误列表在很大程度上是独立于编程语言的,也就是说,大多数的错误都可能出现在用任意语言编写的程序中。

数据引用错误:

  1. 是否有引用的变量未赋值或未初始化?
  2. 对于所有的数组引用,是否每一个下标的值都在相应维规定的界限之内?
  3. 对于所有的数组引用,是否每一个下标的值都是整数?
  4. 对于所有的通过指针或引用变量的引用,当前引用的内存单元是否分配?
  5. 如果一个内存区域具有不同属性的别名,当通过别名进行引用时,内存区域中的数据值是否具有正确的属性?
  6. 变量值的类型或属性是否与编译器所预期的一致?
  7. 在使用的计算机上,当内存分配的单元小于内存可寻址的单元大小时,是否存在直接或间接的寻址错误?
  8. 当使用指针或引用变量时,被引用的内存的属性是否与编译器所预期的一致?
  9. 假如一个数据结构在多个过程或子程序中被引用,那么每个过程或子程序对该结构的定义是否都相同。
  10. 如果字符串有索引,当对数组进行索引操作或下标引用,字符串的边界取值是否有“仅差一个(off-by-one)”的错误?
  11. 对于面向对象的语言,是否所有的继承需求都在实现类中得到了满足?

数据声明错误:

  1. 是否所有的变量都进行了明确的声明?
  2. 如果变量所有的属性在声明中没有明确说明,那么默认的属性能否被正确理解?
  3. 如果变量在声明语句中被初始化,那么它的初始化是否正确? 在很多语言中,数组和字符串的初始化比较复杂,因此也成为容易出错的地方。
  4. 是否每个变量都被赋予了正确的长度和数据类型?
  5. 变量的初始化是否与其存储空间的类型一致?
  6. 是否存在着相似名称的变量?

运算错误:

  1. 是否存在不一致的数据类型(如非算术类型)的变量间的运算?
  2. 是否有混合模式的运算?例如,将浮点变量与一个整型变量做加法运算。这种情况并不一定是错误,但应该谨慎使用。
  3. 是否有相同数据类型不同字长变量间的运算?
  4. 赋值语句的目标变量的数据类型是否小于右边表达式的数据类型或结果?
  5. 在表达式的运算中是否存在表达式向上或向下溢出的情况,也就是说,最终的结果看起来是个有效值,但中间结果对于编程语言的数据类型可能过大或过小。
  6. 除法运算中的除数是否可能为 0?
  7. 在特定场合,变量的值是否超出了有意义的范围?
  8. 对于包含一个以上操作符的表达式,赋值顺序和操作符的优先顺序是否正确?
  9. 整数的运算是否有使用不当的情况,尤其是除法?举例来说.如果 i 是一个整型变量,表达式 2*i/2 == i 是否成立,取决于 i 是奇数还是偶数,或是先运算乘法,还是先运算除法。

比较错误:

  1. 是否有不同数据类型的变量之间的比较运算,例如,将字符串与地址、日期或数字相比较?

  2. 是否有混合模式的比较运算,或不同长度的变量间的比较运算?如果有,应确保程序能正确理解转换规则。

  3. 比较运算符是否正确?程序员经常混淆“至多”、“至少”、“大于”、“不小于”、“小于”和“等于”等比较关系。

  4. 每个布尔表达式所叙述的内容是否都正确?在编写涉及“与”、“或”或“非”的表达式时,程序员经常犯错。

  5. 布尔运算符的操作数是否是布尔类型的?比较运算符和布尔运算符是否错误地混住了一起?

  6. 在二进制的计算机上,是否有用二进制表示的小数或浮点数的比较运算?由于四舍五入,以及用二进制表示十进制数的近似度,这往往是错误的根源。

  7. 对于那些包含一个以上布尔运算符的表达式,赋值顺序以及运算符的优先顺序是否正确?

  8. 编译器计算布尔表达式的方式是否会对程序产生影响?

控制流程错误:

  1. 如果程序包含多条分支路径,比如有计算 GOTO 语句,索引变量的值是否会大于可能的分支数量?
  2. 是否所有的循环最终都终止了?应设计一个非正式的证据或论据来证明每一个循环都会终止。
  3. 程序、模块或子程序是否最终都终止了?
  4. 由于实际情况没有满足循环的入口条件,循环体是否有可能从未执行过?
  5. 如果循环同时由迭代变量和一个布尔条件所控制(如一个搜索循环),如果循环越界(fall-through)了,后果会如何?
  6. 是否存在“仅差一个”的错误,如迭代数量恰恰多一次或少一次?这在从 0开始的循环中是常见的错误。我们会经常忘记将“0”作为一次计数。
  7. 如果编程语言中有语句组或代码块的概念(例如 do-while 或{…}),是否每一组语句都有一个明确的 while 语句,并且 do 语句也与其相应的语句组对应?或者,是否每一个左括号都对应有一个右括号?目前的大多数编译器都能识别出这些不匹配的情况。
  8. 是否存在不能穷尽的判断?

接口错误:

  1. 被调用模块接收到的形参(parameter)数量是否等于调用模块发送的实参(argument)数量?另外,顺序是否正确?
  2. 实参的属性(如数据类型和大小)是否与相应形参的属性相匹配?
  3. 实参的量纲是否与对应形参的量纲相匹配?举例来说,是否形参以度为单位而实参以弧度为单位?
  4. 此模块传递给被模块的实参数量,是否等于被模块期望的形参数量?
  5. 此模块传递给彼模块的实参的属性,是否与彼模块相应形参的属性相匹配?
  6. 此模块传递给彼模块的实参的量纲,是否与彼模块相应形参的量纲相匹配?
  7. 如果调用了内置函数,实参的数量,属性,顺序是否正确?
  8. 如果某个模块或类有多个入口点,是否引用了与当前入口点无关的形参?
  9. 是否有子程序改变了某个原本仅为输入值的形参?
  10. 如果存在全局变量.在所有引用它们的模块中,它们的定义和属性是否相同?
  11. 常数是否以实参形式传递过?

输入输出错误:

  1. 如果对文件明确声明过,其属性是否正确?
  2. 打开文件的语句中各项属性的设置是否正确?
  3. 格式规范是否与 I/O 语句中的信息相吻合?
  4. 是否有足够的可用内存空间,来保留程序将读取的文件?
  5. 是否所有的文件在使用之前都打开?
  6. 是否所有的文件在使用之后都关闭了?
  7. 是否判断文件结束的条件,并正确处理?
  8. 对 I/O 出错情况处理是否正确?
  9. 任何打印或显示的文本信息中是否存在拼写或语法错误?

其他检查:

  1. 如果编译器建立了一个标识符交叉引用列表,那么对该列表进行检查,查看是否有变量从未引用过,或仅被引用过一次。
  2. 如果编译器建立了一个属性列表,那么对每个变量的属性进行检查,确保没有赋予过不希望的默认属性值。
  3. 如果程序编译通过了,但计算机提供了一个或多个“警告”或“提示”信息,应对此逐一进行认真检查。“警告”信息指出编译器对程序某些操作的正确性有所怀疑,所有这些疑问都应进行检查。“提示”信息可能会罗列山没有声明的变量,或者是不利于代码优化的用法。
  4. 程序或模块是否具有足够的鲁棒性?也就是说,它是否对其输入的合法性进行了检查?
  5. 程序是否遗漏了某个功能?

同行评分是一种依据程序整体质量,可维护性、可扩展性、易用性和清晰性对匿名程序进行评价的技术。该项技术的目的是为程序员提供自我评价的手段。

评审人要考虑以下问题:

• 程序是否易于理解?
• 高层次的设计是否可见且合理?
• 低层次的设计是否可见且合理?
• 修改此程序对评审者而言是否容易?
• 评审者是否会以编写出该程序而骄傲?


六、测试用例的设计

软件测试中最重要的因素是设计和生成有效的测试用例。

没有人曾承诺说:软件测试会是容易的事。引用一位智者的话,“如果你觉得设计和编写程序很困难,你就并非一无所知。”

推荐的步骤是先使用黑盒测试方法来设计测试用例,然后视情况需要使用白盒测试方法来设计补充的测试用例。

完全的白盒测试是将程序中每条路径都执行到,然而对一个带有循环的程序来说,完全的路径测试并不切合实际。

逻辑覆盖测试:

判定覆盖或分支覆盖是较强一些的逻辑覆盖准则。该准则要求必须编写足够的测试用例,使得每一个判断都至少有一个为“真”和为“假”的输出结果。换句话说,也就是每条分支路径都必须至少遍历一次。

判定/条件覆盖准则的一个缺点是尽管看上去所有条件的所有结果似乎都执行到了,但由于有些特定的条件会屏蔽掉其他的条件,常常并不能全部都执行到。

对于包含每个判断只存在一种条件的程序,最简单的测试准则就是设计出足够数量的测试用例,实现:(1)将每个判断的所有结果都至少执行一次;(2)将所有的程序入口都至少调用一次,以确保全部的语句都至少执行一次。而对于包含多重条件判断的程序,最简单的测试准则是设计出足够数量的测试用例,将每个判断的所有可能的条件结果的组合,以及所有的入口点都至少执行一次(加入“可能”二字,是因为有些组合情况难以生成)。

等价划分:

当测试某个程序时,我们就被限制在从所有可能的输入中努力找出某个小的子集。理所当然,我们要找的子集必须是正确的,并且是可能发现最多错误的子集。

到一个精心挑选的测试用例还应具备另外两个特性:

  1. 严格控制测试用例的增加,减少为达到“合理测试”的某些既定日标而必须设计的其他测试用例的数量。
  2. 它覆盖了大部分其他可能的测试用例。也就是说,它会告诉我们,使用或不使用这个特定的输入集合,哪些错误会被发现,哪些会被遗漏掉。

使用等价类来生成测试用例,其过程如下:

  1. 为每个等价类设置一个不同的编号。
  2. 编写新的测试用例,尽可能多地覆盖那些尚未被涵盖的有效等价类,直到所有的有效等价类都被测试用例所覆盖(包含进去)。
  3. 编写新的用例,覆盖一个且仅一个尚未被覆盖的无效等价类,直到所有的无效等价类都被测试用例所覆盖。

边界值分析:

边界条件,是指输入和输出等价类中那些恰好处于边界、或超过边界、或在边界以下的状态。

边界值分析方法与等价划分方法存在两方面的不同:

  1. 与从等价类中挑选出任意一个元素作为代表不同,边界值分析需要选择一个或多个元素,以便等价类的每个边界都经过一次测试。
  2. 与仅仅关注输入条件(输入空间)不同,还需要考虑从结果空间(输出等价类)设计测试用例。

测试策略:

  1. 如果规格说明中包含输入条件组合的情况,应首先使用因果图分析方法 。
  2. 在任何情况下都应使用边界值分析方法。
  3. 应为输入和输出确定有效和无效等价类。
  4. 使用错误猜测技术增加更多的测试用例。
  5. 针对上述测试用例集检查程序的逻辑结构。应使用判定覆盖、条件覆盖、判定/条件覆盖或多重条件覆盖准则(最后的一个最为完整)。如果覆盖准则未能被前四个步骤中确定的测试用例所满足,并且满足准则也并非不
    能(由于程序的性质限制,某些条件的组合也许是不可能实现的),那么加足够数量的测试用例,以使覆盖准则得到满足。

七、模块(单元)测试

增量测试:

软件测试是否应先独立地测试每个模块,然后再将这些模块组装成完整的程序?还是先将下一步要测试的模块组装到测试完成的模块集合中,然后再进行测试?第一种方法称为非增量测试或“崩溃(big-bang ) ”测试,而第二种方法称为增量测试或集成。

增量测试的优点:

  1. 可以较早地发现模块中与不匹配接口、不正确假设相关的编程错误。因为尽早地对模块组合进行了集成测试。

  2. 调试会进行得容易一些,我们假定存在着与模块间接口或假设相关的编程错误(根据经验而来的合理假设),那么,如果使用非增量测试,直到整个程序组装之后,这些错误才会浮现出来。

  3. 增量测试会将测试进行得更彻底。增量测试使用先前测试过的模块,取代了非增量测试中使用的桩模块或驱动模块。因此,到最后一个模块测试完成时,实际的模块经受到了更多的检验。

自顶向下测试与自底向上测试:

首先,“自顶向下的测试”、“自顶向下的开发”和“自顶向下的设计”常用作近义词。“自顶向下的测试”和“自顶向下的开发”确实是同义词(表示安排模块的编码和测试顺序的策略),但“自顶向下的设计”则完全不同并且是独立的概念,按自顶向下模式设计的程序既可使用自顶向下的方式,也可使用自底向上的方式进行增量测试。

其次,自底向上的测试(或自底向上的开发)常被错误地当作非增量测试。原因在于自底向上的测试的开展方式与非增量测试是相同的(即对底层或终端模块进行测试),但是就如我们从上一节看到的那样,自底向上的测试是一种增量测试。

自顶向下的测试是从程序的顶部或初始模块开始。测试开始之后,挑选哪一个后续模块进行增量测试没有惟一正确的方法:惟一的原则是:要成为合乎条件的下一个模块,至少一个该模块的从属模块(调用它的模块)事先经过了测试。

不存在最佳的模块序列,但却有下面可供考虑的两项指南:

  1. 如果程序中存在关键部分,那么在设计模块序列时就应将这些关键模块尽可能早地添加进去。所谓“关键部分”可能是某个复杂的模块、某个采用新算法的模块或某个被怀疑容易发生错误的模块。
  2. 在设计模块序列时,应将 I/O 模块尽可能早地添加进来。

当测试用例造成模块输出的实际结果与预期结果不匹配的情况时,存在两个可能的解释:要么该模块存在错误,要么预期的结果不正确(测试用例不正确)。


八、其他测试相关

当程序无法实现其最终用户要求的合理功能时,就发生了一个软件错误。

软件开发过程在很大程度上是沟通有关最终程序的信息、并将信息从一种形式转换到另一种形式。由于这个原因,绝大部分软件错误都可以归因为信息沟通和转换时发生的故障、差错和干扰

软件开发流程:

最终用户 --> 需求 --> 目标 --> 外部规格说明 --> 系统设计 --> 程序结构设计 --> 模块接口规格说明 --> 代码

一个软件产品开发周期的模型, 过程的流程可归结为以下 7 个步骤:

  1. 将软件最终用户的要求转换为一系列书面的需求。这些需求就是该软件产品要实现的目标。

  2. 通过评估可行性与成本、消除相抵触的用户需求、建立优先级和平衡关系,将用户需求转换为具体的目标。

  3. 将上述目标转换为一个准确的产品规格说明,将产品视为一个黑盒,仅考虑其接口以及与最终用户的交互。该规格说明被称为“外部规格说明”。

  4. 如果该产品是一个系统,如操作系统、飞行控制系统、数据库管理系统或雇员人事系统等,而不仅是一个程序(编译器、工资程序、字处理程序等),那么下一步骤就是系统设计。该步骤将系统分割为单独的程序、部件或子系统,并定义它们的接口。

  5. 通过定义每个模块的功能、模块的层次结构以及模块间的接口,来设计程序或程序集合的结构。

  6. 设计一份准确的规格说明,定义每个模块的接口与功能。

  7. 经过一个或更多的子步骤,将模块接口规格说明转换为每个模块的源代码算法。
    以下是从其他角度来审视上述文档的形式:
    • 需求规格说明定义了为什么要开发程序。
    • 目标定义了程序要做什么,以及应做得怎样。
    • 外部规格说明定义了程序对用户的准确表现。
    • 与后续阶段相关的文档越来越详细地规定了程序是如何建立起来的。

假定软件开发周期的七个阶段包括了信息的沟通、理解和转换,以及大多数的软件错误都来源于信息处理中的故障,那么现在有三个补充的方法来预防或识别这些错误。

  • 首先,我们可以使软件开发过程更加精密,以防其中出现很多错误;
  • 其次,在每个阶段结束时可以引入一个独立的验证过程,在进入下一个阶段之前尽可能多。
  • 最后,是对不同的开发阶段采用不同的测试方法。也就是说,将每一个测试过程都重点针对一个特定的转换步骤,从而也针对一类具体的错误。地发现问题。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值