软件测试杂谈

测试自动化

这里先暂且放下 BDD、TDD 等进阶的软件工程理论,讲到测试,许多人的刻板印象,第一个想到的就是手动式的测试方法。把程序执行起来,照着几个情境人工触发程序处理进程,看看程序的行为、输出的结果符不符合预期,有问题的话就修改程序再周而复始相同的过程。但其实这是一个很吊诡的现象,程序开发的用意是要协助人们将工作自动化,以减少人力的负载。结果我们有能力帮别人将工作自动化,而自己做的工作却仍然在使用落后的人工处理!如果让客户知道,客户不会怀疑我们的程序能用吗? 就像做餐饮,如果连自己做出来的餐点自己都不吃,会有客人想吃吗?

再者,考虑到修改程序后,处理源代码间可能出现相互干扰的情况,回归测试是一个必要的程序。也就是改完了程序,最好再将之前测试过的情境再重新走过一次,以确认新的源代码没有影响到程序其他的部份。就如同一般人工和自动化之间的差别,如果是用人工,绝大部份的人就算在没有时间压力的情况之下,想到要全部重来就应该会意兴阑珊,然后只确认跟源代码修改有关的情境。

在不考虑回归测试的情况下,当程序的规模愈大,要涵盖源代码逻辑的测试路径数目会呈现对数成长。也就是说如果要确保程序的品质,要走过的情境就会出现爆炸性的成长。这并不是单纯的人力可以负荷得来的,就算是项目有预算成立专责的测试团队,不用自动化的方式测试也只是加速人力的耗损罢了!

有经手过大型程序开发经验的人就应该会清楚,有很多时候程序问题出现在数据处理区块的深处,要进行测试经常要先执行前置的程序、以制造产生问题的数据状态或执行条件。往往这个过程会需要很多的步骤,同时很多时候也要等待程序进行处理。以手动的方式来测试,为了确认一次程序修正后的结果,就必须要耗掉一段不小的时间,而且大多数都是人被绑在计算机前等待程序回应、以便输入下一个步骤指令。如果修正的结果不如预期,整个程序又要再来一次,如此不断的反覆。工作的效率往往因此而被拖垮,更不用说是要进行先前提到的回归测试。此时,不负责任的工程师因为怠惰会只修改源代码,就自以为程序一定可以正确的运作而结束工作。负责任的工程师则在被要求工作效率的压力之下,急就章地做简单的确认,心虚地交出工作成果。

如此程序的品质怎么可能会好,就算是在这种情况下能够有好的品质,那也只是负责的人有足够的经验在程序开发时回避掉潜在的问题。而这类型的开发项目所会面临的风险不是产出品质不稳定、使用者被迫成为测试系统问题的白老鼠;就是品质全依赖在特定的人身上,一但负责的人收手,项目产出的品质就有可能因此崩溃。

所以要有效率地提升程序产出的品质,自动化测试是必要的开发程序,不管是在多人协同合作的团队或是一个人独立负责的项目里。一旦测试的流程自动化,不论是初次完成的程序或是后续的修改,只要启动自动化的流程,接下来就剩等待工具所产出的报告,中间的过程完全不需要人工介入,当然生活的品质也可以因此而提升,因为不必要再胶着于机械、重覆、耗时的工作上。

要做到测试自动化,当然就是要写测试用的源代码来检验正式的程序是否符合需求。认为撰写测试程序是重工、浪费时间的人,应该要试着放下意气之争,撰写测试源代码的确会增加起始阶段的工作时间,然而一旦测试程序完成之后是会大大地有助于缩短工作时间。坚持 “与其花时间在撰写测试程序,还不如手动跑几个情境来得有效率” 的人,并不适合去担任稍具规模之程序开发的工作,这样的理论只能被应用在微型、不需要与人合作的程序开发工作上,而这样的思维在团队中只是把自己的责任转移给其他人、只会增加其他成员的负担。

在习惯了没有测试源代码的开发方式之后,要改变工作的习性照着 TDD 的理论先去撰写测试程序,的确是会有很大的心理障碍。但是其实可以反过来思考,在写完程序之后、进行手动测试前,想像着把手动执行的步骤给程序化就可以每次完成修改后一键测试,应该可以形成撰写测试源代码的动力。尤其是对没有图形介面的程序测试来说,动机应该会更强烈。有图形介面的程序用手动的方式来进行测试是很直观的,但是对没有图形界面的程序,例如:Web Service 来说就会出现问题。因为人工无法直接触发程序,所以就必须要额外制作一个模拟用的 Client 来送出指令,严格地来说这就是测试程序的一种了。要开始测试了才写测试源代码,也许不符合 TDD 的概念,但最少跨出第一步,习惯之后调整先后次序就仅是次要的议题了。

Code Coverage

有了自动化的测试程序之后,接下来会面临到的议题是“要测试到什么程度才足够?”。“测试的结果有没有通过”是自动化测试里很基本的指标,但是如果测试的源代码跟手动测试一样只是随机挑几个情境来进行,会让测试的结果偏离程序品质的实际情况。试想,如果要检查一个房子的结构,抽查四根柱子是否会比只抽查一根柱子的结果要来得能让人信服。所以这时候就会有另外一项指标是 Code Coverage,这个指标代表了正式源代码在执行测试的过程中有多少部份被执行过。如果 Code Coverage 只有 1%,就算测试结果是通过的,也不代表程序是没有问题的,毕竟还有 99% 是没有被确认过的。

回到原本的问题,要测试到什么程度其实要看项目对于品质的接受程度,并且由相关的负责人员来设定标准。就像之前提到的,程序的规模愈大,要涵盖源代码逻辑的测试路径数目会呈现对数成长。所谓的测试路径,用简化的说法就是在一连串的“源代码逻辑判断式”中,“程序执行经过的判断式条件区块之组合”就是测试路径。也就是如果源代码里有二个 If-Else 的判断式,测试的路径最少会有四条。如此可以想见,当源代码里的逻辑判断式愈多,所会产生的测试路径数目就会开始急速的膨胀。

如果项目要求程序的可靠度到达到百分之百,那所有的测试路径就必须要确认过,以保证程序在任何情况下都是可以正确地运作。只是这会付出极大的成本,因为要测试的情境数可能会是个天文数字。会达到这种程度要求的,大概也只有发射火箭上太空之类的项目负担得起测试成本。一般的项目能够有资源投入测试工作已经是百里挑一了,所以合理地订定指标数值才是重点。

自动化测试的一项优势是可以在付出相对小的成本来涵盖大量的测试情境,像是使用 Data Driven 的方式来以大量的数据验证程序的数据处理源代码。但也不可能无限上纲,以为用自动化测试就可以达到全路径的测试,因为分析测试路径并设计测试数据不是一件简单的工作。

Code Coverage 就成为了检视测试程度的基本指标,一个经过量化以数字来表达测试被涵盖了多少部份的指标。Code Coverage 达到 100% 是不是就代表测试是完美的全路径?不是!其实光 Code Coverage 到达到 100% 就是一件很不容易的事,先不说程序有很多防呆源代码是不好去做出有效的模拟数据,就连程序语言本身都会有一些细节问题会让你无法达成 100% 的 Code Coverage。

以下方的 Pseudo Code 为例:

If a > b

    c = a / b

else

    c = a / 2

当测试程序设定 a = 4, b = 2 及 a = 4, b = 8 二组数据可以让这段源代码的 Code Coverage 达到百分之百,但是程序就万无一失了吗?不是!因为逻辑上 b = 0 时会让程序报错导致执行中断,明显地程序有瑕疪但测试报告并没有办法显现出来,所以 100% 的 Code Coverage 并不表示测试是完整的。

那 Code Coverage 的指标到底有什么意义?我的经验是最少这项指标可以协助我们确认“源代码有多少比例是可被执行的”。这句话看起来好像有点多余,源代码不能执行的话早就被 Compiler 给挡了下来,应该不用到测试阶段吧!Compiler 挡下的语法上的错误,就是所谓的编译时期的错误,而 Runtime Error 则是要执行时才会出现。以上的 Pseudo Code 就是一个很典型的例子,源代码隐含了 Runtime Error,但在编译时期却不见得会显示错误。

根据墨菲定律,程序会出现错误的地方都是没有被确认过的部份。很多程序设计师在写完程序后,大多的心态都是“我写的程序一定不会有错”。就算是会花时间确认程序运行结果,也都只聚焦在主要流程或是常出现的数据状态。所以很多程序平常都是好好的,但是一旦出现不常见的情况或是数据状态时,程序就会产生执行时期的错误。所以尽可能地提高 Code Coverage 对程序品质是有一定的助益,也可以用来协助检视测试数据的样本量是否有达成目标。

以团队角色来看,撰写程序的人在把程序交给测试人员之前,最少应该要做到确认自己的程序不会一执行就当掉,基本的错误处理有被撰写在源代码中,否则就算程序勉强进到测试阶段也只是浪费其他人的时间。

单元还是集成?

在撰写测试程序时,很多时候会纠结在到底是要写单元测试?还是集成测试?是要以白箱的方式来做?还是以黑箱的式来做?如果是选择写单元测试,初次接触的人选定单元的方式大多都是用 Class 来成为单元的基准,因为 Class 的界线明确,也是 OOP 在集结源代码、表达设计内容的单位。如果以此为基准,要做到纯理论的单元测试基本上是不太可能的,单元测试是期望把所要测试的单元与外界的交互关系完全隔离掉,只确认单元内部的运作是否符合设计的内容。

但要完全隔离一个 Class 谈何容易,真正能够只做单纯地数据处理的 Class 毕竟只是系统中不同类型的其中一种,而不管是哪一种类型的 Class 或多或少都会需要引用其他的 Class,尤其是控制类型的 Class。所以有很多的 Test Framework 会利用像是语言的特性来建立 Mock 或 Stub,以便取代非测试对象的 Class 来进行隔离。

要搭配 Mock 之类的技术来做单元测试,使用 IoC 或是 DI 的概念来设计 Class 是个可以达到有效隔离的方式,但却也不是完全没有缺点。过度的设计相对地会使设计的内容过于抽象、破碎,会增加许多额外的开发工作外,也提高新进人员的学习曲线等维护成本。

即使是透过这些技术,要做到百分之百的隔离还是不太可能的,就算是把自行设计的 Class 隔离掉了,但单元中使用的不管是内建还是第三方函式库提供的 Class 都不属于测试的范围。依照理论这些被引用的 Class 应该也要被隔离吧?但实际的情况是,就算有办法隔绝,做到这种程度测试应该也进行不下去了,因为要测试的程序区块可能在被大量的假 Class 取代后失去了功用,测到的大概只剩单行单行的指令。

会形成这样的困境其实是一开始就被所谓单元的范围给限制住了,在单元测试的理论上并没有要求一定要以 Class 为单位,所以并不是毫无差别地隔离所有引用到的 Class 就叫单元测试。单元测试的概念应该是来自于电子电路的设计,以一块电路板来说,单元指的是这个电路板的话,电路板上所使用的电子元件都是测试的范围,不需要管是自行设计还是采购来的。

如同之前提到的,测试的内容是基于项目所能承受的成本而定。每一个项目都是独特的,对每一个项目来说 “没有最佳的方案、只有最适合的方案” 。限定单元只能是一个 Class 也许是最佳的方案,但不见得是最适合的。也许把设计上的一个 Subsystem 当作是一个单元是个相对合适的方式;或是以一个 Class 为主,所有被其引用的 Class 都属于同一个单元。只不过这样的模式就没有像以 Class 为单元那样容易管理测试的资讯,在没有其他资讯的协助之下单元的界线不再那么地明确,有没有单元未被测试所涵盖,最直接有效的资讯大概就只有 Code Coverage 了。

另一个衍生的问题是:单元测试和集成测试的界线又在哪里?把整个系统当作一个单元、网络和数据库都包含在里面,不行吗?这样的话单元和集成测试跟本就是一样的,不是吗?举例的问题夸张了点,稍微大一点的系统应该都有办法做拆解,不致于拿整个系统做单元。万一非不得已,好歹也要想办法把数据库和网络隔离开来,毕竟不太可能在真实的环境中执行测试。

难道就不会有单元是一定要绑着与测试标的无关的外界系统吗,例如:数据库或是网络?我想,有吧!在我们的周遭应该还存在着不少这样的系统。那到底是要做单元测试?还是要做集成测试?正统科班背景出身或是对于理论娴熟的人会说:这是设计所造成的问题,只要正确地设计,单元是单元、整合是整合,不太有机会混为一谈。

非关理论,对于 “要做单元测试还是集成测试”、“单元测试和集成测试的界线” 这些问题,我个人的经验是放下这些名词吧!大部份的时候是无招胜有招,能做到什么程度就做到什么程度。只是要考量到自动化测试的限制,测试应保持独立、并且和真实环境做切割。测试的数据准备程序则是要考虑列在自动化的过程里,以避免不确定的初始数据影响了测试的结果。

很多时候一个团队里的成员素质可能不是那么整齐地都是正统训练出身,甚至多数是半路出家,会打字、会把网络上的范例组装后看起来可以执行的程序设计师。成员不见得了解理论、甚至完全没有接触过,更别提在设计上能应用 IoC、DI;在技术上能清楚并且会使用 Test Framework 里的 Mock、Stub 等功能。团队的管理者在现实中也不见得会有太多的资源来提升成员的素质,或是有足够的时间来投入和成员沟通观念与设计细节。在这样的项目团队里,理论成了高调的空谈,要求成员产出测试则只是避免项目品质过度失控的手段之一,所以思考出一个项目成本可承受的自动化测试模式才是重点。

而不管是使用黑箱还是白箱、单元还是整合,Data Driven 都会是一个必要的工具。有了 Data Driven,可以借由简易的调整数据工作,来达成提高 Code Coverage 的目的,也可以依据问题的发生原因持续累积回归测试所需的样本。一个方便的 Data Driven 工具除了要可以在撰写测试程序时很容易地传入数据,并且要可以在测试的报告中显示出造成测试失败的数据样本。否则只会让测试的结果失去意义,因为只能知道系统有地方不符合预期,但是却没有办法找出问题的根源来加以修正。Data Driven 工具最好还可以有数据产生计划的功能,以便在自动化测试时能够每一次都精确地产生出测试所需的初始数据,甚至可以不用预先制作出大量的初始数据,减少数据储存的成本。

验证及确认

一个分工比较明确的团队在执行测试程序时,应该会引发一个问题是测试的程序是要由 SD 来开发?还是由 PG 来开发?这个问题其实牵涉到了软件工程里所提到的 V&V,也就是验证及确认(Verification and Validation)。对于一个有制度的开发团队来说,这是一个很重要的机制,不管在资安的 ISO 27001 或是软件能力成熟度的 CMMI 都会提到。但很多的项目管理者也许知道要进行测试,但是却很容易轻忽了验证及确认的必要性。

软件开发的目的就是为了要满足使用者的需求,所以在 SA 访谈完、开好了需求的规格之后交给了 SD,SD 依据需求的内容进行规划、完成了系统规格的设计,接下来就是由 PG 来依设计的内容实作出程序。在这一整个过程里,就算是做了测试并且通过,还是会有一个很大的风险:程序不是使用者要的。为什么?怎么会?不是都照着做了吗?对!问题就出在有谁来确认后一个阶段的产出是照着前一个阶段的要求来做的?

每一个人的成长、教育背景或多或少都有差异,而对语言、文字的理解也会有一定的落差,更别提长期爆肝工作下出现的精神错乱,所以在每个阶段的交替间一定会有机会发生不一致的情况。如果测试程序交由 PG 来开发,会形成球员兼裁判的现象,自己测自己的东西很容易会出现盲点,当然也更不可察觉自己对设计内容的"误解"。在负负得正的情况下,产出了一份通过测试的报告。

每一个开发环节上的认知落差,最后产出的程序可能就会和使用者期望有着天差地别的距离。就像是打靶,枪口差个几厘米子弹可能就由靶心飞到另一个靶上去。而在资安上会面临的风险是:也许程序满足了使用者的需求,但是怎么保证系统没有被置入预期之外的后门或是潜藏的弱点?

依照开发的 V-Model 所讲述的内容,每一个开发的阶段都要有其对应的验证和测试工作。验证最直接的方式当然就是静态的审查,以源代码来说 Code Review 是常见的手法。就像一开始说的,毕竟是人工,花费时间又容易出错,可以用程序来取代当然是比较理想的做法。举例来说,Visual Studio 就有提供功能,在绘制 UML 图形时把源代码的 Class 与图形做关连来进行验证,可以用来确认源代码是否有依照循序图的内容来实作。

如果没有工具的协助,我过去的经验中还可以使用 Reflection 的技术,由 SD 依照序列图撰写测试程序来检查 Method 中呼叫的内容是否符合规格。可以达到和某些 Test Framework 里提供透过 Mock 来收集呼叫过程相似的效果,但终究还是属于静态扫描的一种,只是以程序来代替人工的 Code Review。所以有一些动态的资讯,例如:透过回圈被呼叫的次数,是没有办法在测试中确认。

所以 PG 不用写测试程序?我想应该是要看 PG 该写什么类型的测试程序,就像之前提到的 PG 应该对写出来的程序负起责任,最少要有依据来证明所写的源代码都有被试着执行过,而且没有明显的问题。而 SD 应该要有方法能够确认 PG 所产出的程序是依照规格实作的,写测试程序则是可行而且有效率的方法之一。当然这一项工作在一人分饰多角的团队里会看起来很没有意义,程序明明就是自己设计、自己写源代码,哪来的沟通问题,还另外再写程序来检查自己?又不是时间太多?

这个问题以 SDLC 的观点来看,会呈现不同的意义。软件的生命周期在其被宣告死亡、下线之前,之所以被认定为活着的软件是因为被需要、被使用。既然是被持续地使用,会有需求改变的情况也是很常见的,如果软件无法回应需求的变化,那就会逐渐地不被需要、无法使用,而走向死亡。所以软件要持续活着,不断的修改是一个过程,就算开发时只有一个人统筹所有的工作,在后续的修改过程中还是有可能改由不同的人接手。可以有一个修改后确认的依据,哪怕只是简单的确认呼叫的次序是否符合规格,对品质的确保都有一定的助益。最少在测试不通过的当下,可以察觉程序是有工作还没有完成的,不管是正式或测试的源代码要被调整。

同场加映

其实在不受限于组织规范要求的情况下,BDD、TDD 配合 Scrum 的开发模式是很值得参考的,可以让测试的工作更有效地被落实在开发流程中。相较于传统的瀑布式开发流程,导入这些开发的理论可以展现出较快速、较有弹性的使用者需求调整,而不需要再透过冻结需求、繁复的确认、长时间实作等流程来成为不利开发项目进行的因素。由于交付与确认都是相对地即时,使用者可以在一个个合理时间区间的 Sprint 后实际操作所期望之系统对应功能,可以大大的减少产出的结果不符合预期的风险,同时也提高了结案的机率。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707
  • 文章来自:http://blog.csdn.net/mj813/article/details/52451355 ...
    好大一只鹏阅读 9,158评论 2 126
  • 本着共享主义,本人将PPT考点梳理出来,并且已经翻译成中文,供大家参考,欢迎各位指导! 本次考试题型分为选择、判断...
    Moonsmile阅读 4,027评论 13 27
  • 真不想承认,我和她有太多共同点。 这些共同点决定了我们在一起相处的状态。 所以我们经常会因为某句话某个人某件很小的...
    小笑蜜蜜阅读 345评论 0 0