单元测试
上一篇 / 下一篇 2006-12-13 19:46:37 / 个人分类:软件测试技术
51Testing软件测试网,qq.x"H8Tx0^3S
51Testing软件测试网F _,l`'C
单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件 (或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。51Testing软件测试网v%nV.N}
/hjx9C:NlU4b-u0 单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
(l-H L1w&?}S2j/Y051Testing软件测试网9[v6A1MBM0^"K
工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。
@v {6R Yr0
g-K+rY0]s0 其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么 的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未 覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。 可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
{'nua/acNW0
"F1?Jx4D0 对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
Y@*m!z{t!eo0
gUl@\ Q$t/T3a0 要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。
JM%`)kA7VaY.?0
3L%j:^8GOW0 一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位, 复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因 为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
:~ N-UK.I#S-K051Testing软件测试网bMK'Rmz!Q'A
有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包 含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在 头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。51Testing软件测试网@ScsD8G ?'L,m
D!@tF \+C5A0为什么要使用单元测试
9Oom)sNnc/wyA0R0
!I {:[5e(^0 我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。51Testing软件测试网U:k)g?.K IA
Ab,id3e8Q4SuqH*x0 幸运,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。51Testing软件测试网qO8S]%}"`:C
7I\H:K[GOy%w `0 什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过 分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产 品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后 再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
~YwtH#S0
+I$V*o-w9E3rd`8q0 由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。51Testing软件测试网$P)K3NT2zF&G
p8M9{c'om*_e~j5{0 关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩 函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减 少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。51Testing软件测试网wg3~1ZLp
51Testing软件测试网gl B4r2n0o@S
在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中,要进行测试的基本单元是类。对Ada语 言来说,开发人员可以选择是在独立的过程和函数,还是在Ada包的级别上进行单元测试。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里 基本单元被典型地划分为一个菜单或显示界面。
([3@%|2lc0
/cU*O~%b C0 单元测试不仅仅是作为无错编码一种辅助手段在一次性的开发过程中使用,单元测试必须是可重复的,无论是在软件修改,或是移植到新的运行环境的过程中。因此,所有的测试都必须在整个软件系统的生命周期中进行维护。
a G+s*qZ}"G6}0
[JxW m/{k"E{0 经常与单元测试联系起来的另外一些开发活动包括代码走读(Code review),静态分析(Static analysis)和动态分析(Dynamic analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。动态分析就是通过观察软件运行时 的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息。51Testing软件测试网e`!}5i8yn S
:mk]V,|3zb[%S.rD7t0一些流行的误解51Testing软件测试网+BZu A]M
/E}'u b6o0 在明确了什么是单元测试以后,我们可以进行"反调论证"了。在下面的章节里,我们列出了一些反对单元测试的普遍的论点。然后用充分的理由来证明这些论点是不足取的。51Testing软件测试网8MO0^.F#^([a#A
c(TH E5N6uI.~0 它浪费了太多的时间51Testing软件测试网5jp6zg)H Uo
一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。这在外表上看来是一项明显的进步,而象单元 测试这样的活动也许会被看作是通往这个阶段点的道路上的障碍,推迟了对整个系统进行联调这种真正有意思的工作启动的时间。51Testing软件测试网XE X5g.`J
51Testing软件测试网o0Xr Df
在这种开发步骤中,真实意义上的进步被外表上的进步取代了。系统能够正常工作的可能性是很小的,更多的情况是充满了各式各样的Bug。在实践中,这样 一种开发步骤常常会导致这样的结果:软件甚至无法运行。更进一步的结果是大量的时间将被花费在跟踪那些包含在独立单元里的简单的Bug上面,在个别情况 下,这些Bug也许是琐碎和微不足道的,但是总的来说,他们会导致在软件集成为一个系统时增加额外的工期, 而且当这个系统投入使用时也无法确保它能够可靠运行。51Testing软件测试网K'vfz(A
g*K j|:} Sy0 在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,在确信他们 手头拥有稳定可靠的部件的情况下,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效的利用。 而调试人员的不受控和散漫的工作方式只会花费更多的时间而取得很少的好处。51Testing软件测试网`}k"q:s/r q
51Testing软件测试网 u2LYkpx5[+xF h2h2\
使用AdaTEST和Cantata这样的支持工具可以使单元测试更加简单和有效。但这不是必须的,单元测试即使是在没有工具支持的情况下也是一项非常有意义的活动。51Testing软件测试网OoB)VJ
51Testing软件测试网3bMW2K`F.r&P9?
它仅仅是证明这些代码做了什么51Testing软件测试网 j R&V0A/Y/G&V
这是那些没有首先为每个单元编写一个详细的规格说明而直接跳到编码阶段的开发人员提出的一条普遍的抱怨,当编码完成以后并且面临代码测试任务的时候, 他们就阅读这些代码并找出它实际上做了什么,把他们的测试工作基于已经写好的代码的基础上。当然,他们无法证明任何事情。所有的这些测试工作能够表明的事 情就是编译器工作正常。是的,他们也许能够抓住(希望能够)罕见的编译器Bug,但是他们能够做的仅仅是这些。51Testing软件测试网 j]{*_vwv:I
51Testing软件测试网4c+k0C$g1|V
如果他们首先写好一个详细的规格说明,测试能够以规格说明为基础。代码就能够针对它的规格说明,而不是针对自身进行测试。这样的测试仍然能够抓住编译 器的Bug,同时也能找到更多的编码错误,甚至是一些规格说明中的错误。好的规格说明可以使测试的质量更高,所以最后的结论是高质量的测试需要高质量的规 格说明。51Testing软件测试网?#Hv^3b*Mt @
51Testing软件测试网8E E6V \8d!A-f1h"R7nj
在实践中会出现这样的情况:一个开发人员要面对测试一个单元时只给出单元的代码而没有规格说明这样吃力不讨好的任务。你怎样做才会有更多的收获,而不 仅仅是发现编译器的Bug?第一步是理解这个单元原本要做什么, --- 不是它实际上做了什么。比较有效的方法是倒推出一个概要的规格说明。这个过程的主要输入条件是要阅读那些程序代码和注释, 主要针对这个单元,及调用它和被它调用的相关代码。画出流程图是非常有帮助的,你可以用手工或使用某种工具。可以组织对这个概要规格说明的走读 (Review),以确保对这个单元的说明没有基本的错误,有了这种最小程度的代码深层说明,就可以用它来设计单元测试了。
6z?P.Z4k/[0Nb0
8r;B-I?Mz$_0 我是个很棒的程序员, 我是不是可以不进行单元测试?51Testing软件测试网!v9O/UQ,gx6ti"s'XQ
在每个开发组织中都至少有一个这样的开发人员,他非常擅长于编程,他们开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?51Testing软件测试网.Eo%[dZ
51Testing软件测试网)m3U S4IEF
在真实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单的程序中应付过去。 但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。
k-[ a4s%XEs051Testing软件测试网4q)GvIYc rzl})s
编码不是一个可以一次性通过的过程。在真实世界中,软件产品必须进行维护以对操作需求的改变作出反应, 并且要对最初的开发工作遗留下来的Bug进行修改。你希望依靠那些原始作者进行修改吗? 这些制造出这些未经测试的原始代码的资深专家们还会继续在其他地方制造这样的代码。在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的 负作用。51Testing软件测试网 w4OL~:[)?s)z
X3A`U\Y0 不管怎样, 集成测试将会抓住所有的Bug51Testing软件测试网/ey$|soFlE
我们已经在前面的讨论中从一个侧面对这个问题进行了部分的阐述。这个论点不成立的原因在于规模越大的代码集成意味着复杂性就越高。如果软件的单元没有事先进行测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。
PM5dvjB0
tK'Zc*H0 一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种 单元调用的测试条件的时候,要全面的考虑单元的被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远的超过独立进行的单元测试过 程。51Testing软件测试网FP8F3Nj+lg3o Y
:A}6{Gg4L#{,m%a0 最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。51Testing软件测试网wK:s*[$SS.|4d
:K:Z4hy|2y;@7M|0 让我们类比一下,假设我们要清洗一台已经完全装配好的食物加工机器!无论你喷了多少水和清洁剂,一些食物的小碎片还是会粘在机器的死角位置,只有任其 腐烂并等待以后再想办法。但我们换个角度想想,如果这台机器是拆开的,这些死角也许就不存在或者更容易接触到了,并且每一部分都可以毫不费力的进行清洗。
o [[6\@r051Testing软件测试网$[5hXg] _m
它的成本效率不高51Testing软件测试网M+^H U tZpQ
一个特定的开发组织或软件应用系统的测试水平取决于对那些未发现的Bug的潜在后果的重视程度。这种后果的严重程度可以从一个Bug引起的小小的不便 到发生多次的死机的情况。这种后果可能常常会被软件的开发人员所忽视(但是用户可不会这样),这种情况会长期的损害这些向用户提交带有Bug的软件的开发 组织的信誉,并且会导致对未来的市场产生负面的影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个开发组织获取未来的市场。
~j k\WM051Testing软件测试网V].b{-\7Sh7S5tj
很多研究成果表明,无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现的 越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早的查找和修改Bug。在修改费用变的过高之前,单元测试是一个在早期抓住Bug的机会。
+Ec"` |5Fl051Testing软件测试网(M|&Qj(A nU"E{:H
相比后阶段的测试,单元测试的创建更简单,维护更容易,并且可以更方便的进行重复。从全程的费用来考虑, 相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说,单元测试所需的费用是很低的。
ijDa}051Testing软件测试网Z@Y}bf
一些图表
#k9K+NkTAl"?0 这些图表摘自<<实用软件度量>>(Capers Jones,McGraw-Hill 1991),它列出了准备测试,执行测试,和修改缺陷所花费的时间(以一个功能点为基准),这些数据显示单元测试的成本效率大约是集成测试的两倍 系统测试的三倍(参见条形图)。
Z FN*EEkGn0
{_1~ a-H4v+hR1}0(术语域测试(Field test)意思是在软件投入使用以后,针对某个领域所作的所有测试活动)51Testing软件测试网'QJi"ien
51Testing软件测试网]W$sSnJ7a8Q#y)P
这个图表并不表示开发人员不应该进行后阶段的测试活动,这次测试活动仍然是必须的。它的真正意思是尽可能早的排除尽可能多的Bug可以减少后阶段测试的费用。51Testing软件测试网 VFj(r'V{]O9c
R"SG4s${g0 其他的一些图表显示高达50%的维护工作量被花在那些总是会有的Bug的修改上面。如果这些Bug在开发阶段被排除掉的话,这些工作量就可以节省下 来。当考虑到软件维护费用可能会比最初的开发费用高出数倍的时候,这种潜在的对50%软件维护费用的节省将对整个软件生命周期费用产生重大的影响。
f;flr0rn H_#k0
be"Pg.e5_P0结论
zm6tk6dU mA051Testing软件测试网Q!vS)UR
经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低。在软件开发的后期阶段,Bug的发现并修改将 会变得更加困难,并要消耗大量的时间和开发费用。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到 最好的保证。在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入 充满很多 Bug的单元之中不能自拔。51Testing软件测试网3z+Z Z2ne
(w8Bz%n2t ]!}*Zc8@w0 使测试工作的效力发挥到最大化的关键在于选择正确的测试策略,这其中包含了完全的单元测试的概念,以及对测试过程的良好的管理,还有适当地使用象 AdaTEST和Cantata这样的工具来支持测试过程。这些活动可以产生这样的结果:在花费更低的开发费用的情况下得到更稳定的软件。更进一步的好处 是简化了维护过程并降低了生命周期的费用。有效的单元测试是推行全局质量文化的一部分,而这种质量文化将会为软件开发者带来无限的商机。
4d||;cr U0
!a8?ID R#d{0单元测试的优点
6mM/Im ?Ap2C0
]'N5o%~ _#e9^01、它是一种验证行为。51Testing软件测试网*A Pr|/ef
程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。51Testing软件测试网.{ _6I2s Bs
51Testing软件测试网1NhNpF0x
2、它是一种设计行为。
"Ha0hE'NH"k0 编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
.f~Q&C$t|x {w051Testing软件测试网W"I#~r4s2`7L/p-s
3、它是一种编写文档的行为。51Testing软件测试网ME?.u F
单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
*O1|AoE*S9n6Xx0z0
0wD|"no$x04、它具有回归性。
mPa w_2XYA0 自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
)[] I%nq_aH z0
:we@NcbZ0单元测试的范畴
BGl0H-Ad0
+xpL0\q p P%@051Testing软件测试网9k4@I9uQ3c
如果要给单元测试定义一个明确的范畴,指出哪些功能是属于单元测试,这似乎很难。但下面讨论的四个问题,基本上可以说明单元测试的范畴,单元测试所要做的工作。51Testing软件测试网z5jD h Bd
51Testing软件测试网3i| tEW&L
1、 它的行为和我期望的一致吗?
5w/r/[}D.w1]E0 这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的。51Testing软件测试网 kfq ZE~ G
51Testing软件测试网pP!Pro~"Q A0l
2、 它的行为一直和我期望的一致吗?51Testing软件测试网8R"Qp[P#f
编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一个项复杂的工程,在测试某段代码的行为是否和你的期望一 致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。51Testing软件测试网$rf%a0lAtZf
3K;uH*c8pneahn+s03、 我可以依赖单元测试吗?
;qqV"~A0 不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。
V%X%A7B"X6d/X0
GM|'mt[hV04、 单元测试说明我的意图了吗?
`SW,f)A0 单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。51Testing软件测试网7XwS6MF/z3h,h
%xY?x(]II0不写测试的借口
"j/WcV/l0
0qK/WU2m.D*E5vlC051Testing软件测试网9K^^CXvk
到这里,我们已经列举了使用单元测试的种种理由。也许,每个人都同意,是的,该做更多的测试。这种人人同意的事情还多着呢,是的,该多吃蔬菜,该戒烟,该多休息,该多锻炼……这并不意味着我们中的所有人都会这么去做,不是吗?
5ULZC%BJ0
O1U/p7hc01、 编写单元测试太花时间了。51Testing软件测试网!yp)}nW ^O
我们知道,在开发时越早发现BUG,就能节省更多的时间,降低更多的风险。51Testing软件测试网{ ?M l6I5L
下图表摘自<<实用软件度量>>(Capers Jones,McGraw-Hill 1991),它列出了准备测试,执行测试,和修改缺陷所花费的时间(以一个功能点为基准),这些数据显示单元测试的成本效率大约是集成测试的两倍,是系统 测试的三倍(参见条形图)。51Testing软件测试网whn2Z,x0q{c
51Testing软件测试网 Q8h.BBk{ s-J
术语:域测试(Field test)意思是在软件投入使用以后,针对某个领域所作的所有测试活动。
@W4EOBDd G;|p0 如果你仍然认为在编写产品代码的时候,还是没有时间编写测试代码,那么请先考虑下面这些问题:51Testing软件测试网E? bgxhxgv
1)、对于所编写的代码,你在调试上面花了多少时间。51Testing软件测试网-_9m-H9kkY
2)、对于以前你自认为正确的代码,而实际上这些代码却存在重大的bug,你花了多少时间在重新确认这些代码上面。
r\&G
51Testing软件测试网F _,l`'C
单元测试(模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常而言,一个单元测试是用于判断某个特定条件 (或者场景)下某个特定函数的行为。例如,你可能把一个很大的值放入一个有序list 中去,然后确认该值出现在list 的尾部。或者,你可能会从字符串中删除匹配某种模式的字符,然后确认字符串确实不再包含这些字符了。51Testing软件测试网v%nV.N}
/hjx9C:NlU4b-u0 单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
(l-H L1w&?}S2j/Y051Testing软件测试网9[v6A1MBM0^"K
工厂在组装一台电视机之前,会对每个元件都进行测试,这,就是单元测试。
@v {6R Yr0
g-K+rY0]s0 其实我们每天都在做单元测试。你写了一个函数,除了极简单的外,总是要执行一下,看看功能是否正常,有时还要想办法输出些数据,如弹出信息窗口什么 的,这,也是单元测试,老纳把这种单元测试称为临时单元测试。只进行了临时单元测试的软件,针对代码的测试很不完整,代码覆盖率要超过70%都很困难,未 覆盖的代码可能遗留大量的细小的错误,这些错误还会互相影响,当BUG暴露出来的时候难于调试,大幅度提高后期测试和维护成本,也降低了开发商的竞争力。 可以说,进行充分的单元测试,是提高软件质量,降低开发成本的必由之路。
{'nua/acNW0
"F1?Jx4D0 对于程序员来说,如果养成了对自己写的代码进行单元测试的习惯,不但可以写出高质量的代码,而且还能提高编程水平。
Y@*m!z{t!eo0
gUl@\ Q$t/T3a0 要进行充分的单元测试,应专门编写测试代码,并与产品代码隔离。老纳认为,比较简单的办法是为产品工程建立对应的测试工程,为每个类建立对应的测试类,为每个函数(很简单的除外)建立测试函数。首先就几个概念谈谈老纳的看法。
JM%`)kA7VaY.?0
3L%j:^8GOW0 一般认为,在结构化程序时代,单元测试所说的单元是指函数,在当今的面向对象时代,单元测试所说的单元是指类。以老纳的实践来看,以类作为测试单位, 复杂度高,可操作性较差,因此仍然主张以函数作为单元测试的测试单位,但可以用一个测试类来组织某个类的所有测试函数。单元测试不应过分强调面向对象,因 为局部代码依然是结构化的。单元测试的工作量较大,简单实用高效才是硬道理。
:~ N-UK.I#S-K051Testing软件测试网bMK'Rmz!Q'A
有一种看法是,只测试类的接口(公有函数),不测试其他函数,从面向对象角度来看,确实有其道理,但是,测试的目的是找错并最终排错,因此,只要是包 含错误的可能性较大的函数都要测试,跟函数是否私有没有关系。对于C++来说,可以用一种简单的方法区隔需测试的函数:简单的函数如数据读写函数的实现在 头文件中编写(inline函数),所有在源文件编写实现的函数都要进行测试(构造函数和析构函数除外)。51Testing软件测试网@ScsD8G ?'L,m
D!@tF \+C5A0为什么要使用单元测试
9Oom)sNnc/wyA0R0
!I {:[5e(^0 我们编写代码时,一定会反复调试保证它能够编译通过。如果是编译没有通过的代码,没有任何人会愿意交付给自己的老板。但代码通过编译,只是说明了它的语法正确;我们却无法保证它的语义也一定正确,没有任何人可以轻易承诺这段代码的行为一定是正确的。51Testing软件测试网U:k)g?.K IA
Ab,id3e8Q4SuqH*x0 幸运,单元测试会为我们的承诺做保证。编写单元测试就是用来验证这段代码的行为是否与我们期望的一致。有了单元测试,我们可以自信的交付自己的代码,而没有任何的后顾之忧。51Testing软件测试网qO8S]%}"`:C
7I\H:K[GOy%w `0 什么时候测试?单元测试越早越好,早到什么程度?XP开发理论讲究TDD,即测试驱动开发,先编写测试代码,再进行开发。在实际的工作中,可以不必过 分强调先什么后什么,重要的是高效和感觉舒适。从老纳的经验来看,先编写产品函数的框架,然后编写测试函数,针对产品函数的功能编写测试用例,然后编写产 品函数的代码,每写一个功能点都运行测试,随时补充测试用例。所谓先编写产品函数的框架,是指先编写函数空的实现,有返回值的随便返回一个值,编译通过后 再编写测试代码,这时,函数名、参数表、返回类型都应该确定下来了,所编写的测试代码以后需修改的可能性比较小。
~YwtH#S0
+I$V*o-w9E3rd`8q0 由谁测试?单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成,也就是说,经过了单元测试的代码才是已完成的代码,提交产品代码时也要同时提交测试代码。测试部门可以作一定程度的审核。51Testing软件测试网$P)K3NT2zF&G
p8M9{c'om*_e~j5{0 关于桩代码,老纳认为,单元测试应避免编写桩代码。桩代码就是用来代替某些代码的代码,例如,产品函数或测试函数调用了一个未编写的函数,可以编写桩 函数来代替该被调用的函数,桩代码也用于实现测试隔离。采用由底向上的方式进行开发,底层的代码先开发并先测试,可以避免编写桩代码,这样做的好处有:减 少了工作量;测试上层函数时,也是对下层函数的间接测试;当下层函数修改时,通过回归测试可以确认修改是否导致上层函数产生错误。51Testing软件测试网wg3~1ZLp
51Testing软件测试网gl B4r2n0o@S
在一种传统的结构化编程语言中,比如C,要进行测试的单元一般是函数或子过程。在象C++这样的面向对象的语言中,要进行测试的基本单元是类。对Ada语 言来说,开发人员可以选择是在独立的过程和函数,还是在Ada包的级别上进行单元测试。单元测试的原则同样被扩展到第四代语言(4GL)的开发中,在这里 基本单元被典型地划分为一个菜单或显示界面。
([3@%|2lc0
/cU*O~%b C0 单元测试不仅仅是作为无错编码一种辅助手段在一次性的开发过程中使用,单元测试必须是可重复的,无论是在软件修改,或是移植到新的运行环境的过程中。因此,所有的测试都必须在整个软件系统的生命周期中进行维护。
a G+s*qZ}"G6}0
[JxW m/{k"E{0 经常与单元测试联系起来的另外一些开发活动包括代码走读(Code review),静态分析(Static analysis)和动态分析(Dynamic analysis)。静态分析就是对软件的源代码进行研读,查找错误或收集一些度量数据,并不需要对代码进行编译和执行。动态分析就是通过观察软件运行时 的动作,来提供执行跟踪,时间分析,以及测试覆盖度方面的信息。51Testing软件测试网e`!}5i8yn S
:mk]V,|3zb[%S.rD7t0一些流行的误解51Testing软件测试网+BZu A]M
/E}'u b6o0 在明确了什么是单元测试以后,我们可以进行"反调论证"了。在下面的章节里,我们列出了一些反对单元测试的普遍的论点。然后用充分的理由来证明这些论点是不足取的。51Testing软件测试网8MO0^.F#^([a#A
c(TH E5N6uI.~0 它浪费了太多的时间51Testing软件测试网5jp6zg)H Uo
一旦编码完成,开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。这在外表上看来是一项明显的进步,而象单元 测试这样的活动也许会被看作是通往这个阶段点的道路上的障碍,推迟了对整个系统进行联调这种真正有意思的工作启动的时间。51Testing软件测试网XE X5g.`J
51Testing软件测试网o0Xr Df
在这种开发步骤中,真实意义上的进步被外表上的进步取代了。系统能够正常工作的可能性是很小的,更多的情况是充满了各式各样的Bug。在实践中,这样 一种开发步骤常常会导致这样的结果:软件甚至无法运行。更进一步的结果是大量的时间将被花费在跟踪那些包含在独立单元里的简单的Bug上面,在个别情况 下,这些Bug也许是琐碎和微不足道的,但是总的来说,他们会导致在软件集成为一个系统时增加额外的工期, 而且当这个系统投入使用时也无法确保它能够可靠运行。51Testing软件测试网K'vfz(A
g*K j|:} Sy0 在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,在确信他们 手头拥有稳定可靠的部件的情况下,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效的利用。 而调试人员的不受控和散漫的工作方式只会花费更多的时间而取得很少的好处。51Testing软件测试网`}k"q:s/r q
51Testing软件测试网 u2LYkpx5[+xF h2h2\
使用AdaTEST和Cantata这样的支持工具可以使单元测试更加简单和有效。但这不是必须的,单元测试即使是在没有工具支持的情况下也是一项非常有意义的活动。51Testing软件测试网OoB)VJ
51Testing软件测试网3bMW2K`F.r&P9?
它仅仅是证明这些代码做了什么51Testing软件测试网 j R&V0A/Y/G&V
这是那些没有首先为每个单元编写一个详细的规格说明而直接跳到编码阶段的开发人员提出的一条普遍的抱怨,当编码完成以后并且面临代码测试任务的时候, 他们就阅读这些代码并找出它实际上做了什么,把他们的测试工作基于已经写好的代码的基础上。当然,他们无法证明任何事情。所有的这些测试工作能够表明的事 情就是编译器工作正常。是的,他们也许能够抓住(希望能够)罕见的编译器Bug,但是他们能够做的仅仅是这些。51Testing软件测试网 j]{*_vwv:I
51Testing软件测试网4c+k0C$g1|V
如果他们首先写好一个详细的规格说明,测试能够以规格说明为基础。代码就能够针对它的规格说明,而不是针对自身进行测试。这样的测试仍然能够抓住编译 器的Bug,同时也能找到更多的编码错误,甚至是一些规格说明中的错误。好的规格说明可以使测试的质量更高,所以最后的结论是高质量的测试需要高质量的规 格说明。51Testing软件测试网?#Hv^3b*Mt @
51Testing软件测试网8E E6V \8d!A-f1h"R7nj
在实践中会出现这样的情况:一个开发人员要面对测试一个单元时只给出单元的代码而没有规格说明这样吃力不讨好的任务。你怎样做才会有更多的收获,而不 仅仅是发现编译器的Bug?第一步是理解这个单元原本要做什么, --- 不是它实际上做了什么。比较有效的方法是倒推出一个概要的规格说明。这个过程的主要输入条件是要阅读那些程序代码和注释, 主要针对这个单元,及调用它和被它调用的相关代码。画出流程图是非常有帮助的,你可以用手工或使用某种工具。可以组织对这个概要规格说明的走读 (Review),以确保对这个单元的说明没有基本的错误,有了这种最小程度的代码深层说明,就可以用它来设计单元测试了。
6z?P.Z4k/[0Nb0
8r;B-I?Mz$_0 我是个很棒的程序员, 我是不是可以不进行单元测试?51Testing软件测试网!v9O/UQ,gx6ti"s'XQ
在每个开发组织中都至少有一个这样的开发人员,他非常擅长于编程,他们开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?51Testing软件测试网.Eo%[dZ
51Testing软件测试网)m3U S4IEF
在真实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单的程序中应付过去。 但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。
k-[ a4s%XEs051Testing软件测试网4q)GvIYc rzl})s
编码不是一个可以一次性通过的过程。在真实世界中,软件产品必须进行维护以对操作需求的改变作出反应, 并且要对最初的开发工作遗留下来的Bug进行修改。你希望依靠那些原始作者进行修改吗? 这些制造出这些未经测试的原始代码的资深专家们还会继续在其他地方制造这样的代码。在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的 负作用。51Testing软件测试网 w4OL~:[)?s)z
X3A`U\Y0 不管怎样, 集成测试将会抓住所有的Bug51Testing软件测试网/ey$|soFlE
我们已经在前面的讨论中从一个侧面对这个问题进行了部分的阐述。这个论点不成立的原因在于规模越大的代码集成意味着复杂性就越高。如果软件的单元没有事先进行测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。
PM5dvjB0
tK'Zc*H0 一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种 单元调用的测试条件的时候,要全面的考虑单元的被调用时的各种入口参数。在软件集成阶段,对单元功能全面测试的复杂程度远远的超过独立进行的单元测试过 程。51Testing软件测试网FP8F3Nj+lg3o Y
:A}6{Gg4L#{,m%a0 最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。51Testing软件测试网wK:s*[$SS.|4d
:K:Z4hy|2y;@7M|0 让我们类比一下,假设我们要清洗一台已经完全装配好的食物加工机器!无论你喷了多少水和清洁剂,一些食物的小碎片还是会粘在机器的死角位置,只有任其 腐烂并等待以后再想办法。但我们换个角度想想,如果这台机器是拆开的,这些死角也许就不存在或者更容易接触到了,并且每一部分都可以毫不费力的进行清洗。
o [[6\@r051Testing软件测试网$[5hXg] _m
它的成本效率不高51Testing软件测试网M+^H U tZpQ
一个特定的开发组织或软件应用系统的测试水平取决于对那些未发现的Bug的潜在后果的重视程度。这种后果的严重程度可以从一个Bug引起的小小的不便 到发生多次的死机的情况。这种后果可能常常会被软件的开发人员所忽视(但是用户可不会这样),这种情况会长期的损害这些向用户提交带有Bug的软件的开发 组织的信誉,并且会导致对未来的市场产生负面的影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个开发组织获取未来的市场。
~j k\WM051Testing软件测试网V].b{-\7Sh7S5tj
很多研究成果表明,无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现的 越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早的查找和修改Bug。在修改费用变的过高之前,单元测试是一个在早期抓住Bug的机会。
+Ec"` |5Fl051Testing软件测试网(M|&Qj(A nU"E{:H
相比后阶段的测试,单元测试的创建更简单,维护更容易,并且可以更方便的进行重复。从全程的费用来考虑, 相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说,单元测试所需的费用是很低的。
ijDa}051Testing软件测试网Z@Y}bf
一些图表
#k9K+NkTAl"?0 这些图表摘自<<实用软件度量>>(Capers Jones,McGraw-Hill 1991),它列出了准备测试,执行测试,和修改缺陷所花费的时间(以一个功能点为基准),这些数据显示单元测试的成本效率大约是集成测试的两倍 系统测试的三倍(参见条形图)。
Z FN*EEkGn0
{_1~ a-H4v+hR1}0(术语域测试(Field test)意思是在软件投入使用以后,针对某个领域所作的所有测试活动)51Testing软件测试网'QJi"ien
51Testing软件测试网]W$sSnJ7a8Q#y)P
这个图表并不表示开发人员不应该进行后阶段的测试活动,这次测试活动仍然是必须的。它的真正意思是尽可能早的排除尽可能多的Bug可以减少后阶段测试的费用。51Testing软件测试网 VFj(r'V{]O9c
R"SG4s${g0 其他的一些图表显示高达50%的维护工作量被花在那些总是会有的Bug的修改上面。如果这些Bug在开发阶段被排除掉的话,这些工作量就可以节省下 来。当考虑到软件维护费用可能会比最初的开发费用高出数倍的时候,这种潜在的对50%软件维护费用的节省将对整个软件生命周期费用产生重大的影响。
f;flr0rn H_#k0
be"Pg.e5_P0结论
zm6tk6dU mA051Testing软件测试网Q!vS)UR
经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低。在软件开发的后期阶段,Bug的发现并修改将 会变得更加困难,并要消耗大量的时间和开发费用。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到 最好的保证。在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入 充满很多 Bug的单元之中不能自拔。51Testing软件测试网3z+Z Z2ne
(w8Bz%n2t ]!}*Zc8@w0 使测试工作的效力发挥到最大化的关键在于选择正确的测试策略,这其中包含了完全的单元测试的概念,以及对测试过程的良好的管理,还有适当地使用象 AdaTEST和Cantata这样的工具来支持测试过程。这些活动可以产生这样的结果:在花费更低的开发费用的情况下得到更稳定的软件。更进一步的好处 是简化了维护过程并降低了生命周期的费用。有效的单元测试是推行全局质量文化的一部分,而这种质量文化将会为软件开发者带来无限的商机。
4d||;cr U0
!a8?ID R#d{0单元测试的优点
6mM/Im ?Ap2C0
]'N5o%~ _#e9^01、它是一种验证行为。51Testing软件测试网*A Pr|/ef
程序中的每一项功能都是测试来验证它的正确性。它为以后的开发提供支缓。就算是开发后期,我们也可以轻松的增加功能或更改程序结构,而不用担心这个过程中会破坏重要的东西。而且它为代码的重构提供了保障。这样,我们就可以更自由的对程序进行改进。51Testing软件测试网.{ _6I2s Bs
51Testing软件测试网1NhNpF0x
2、它是一种设计行为。
"Ha0hE'NH"k0 编写单元测试将使我们从调用者观察、思考。特别是先写测试(test-first),迫使我们把程序设计成易于调用和可测试的,即迫使我们解除软件中的耦合。
.f~Q&C$t|x {w051Testing软件测试网W"I#~r4s2`7L/p-s
3、它是一种编写文档的行为。51Testing软件测试网ME?.u F
单元测试是一种无价的文档,它是展示函数或类如何使用的最佳文档。这份文档是可编译、可运行的,并且它保持最新,永远与代码同步。
*O1|AoE*S9n6Xx0z0
0wD|"no$x04、它具有回归性。
mPa w_2XYA0 自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地的快速运行测试。
)[] I%nq_aH z0
:we@NcbZ0单元测试的范畴
BGl0H-Ad0
+xpL0\q p P%@051Testing软件测试网9k4@I9uQ3c
如果要给单元测试定义一个明确的范畴,指出哪些功能是属于单元测试,这似乎很难。但下面讨论的四个问题,基本上可以说明单元测试的范畴,单元测试所要做的工作。51Testing软件测试网z5jD h Bd
51Testing软件测试网3i| tEW&L
1、 它的行为和我期望的一致吗?
5w/r/[}D.w1]E0 这是单元测试最根本的目的,我们就是用单元测试的代码来证明它所做的就是我们所期望的。51Testing软件测试网 kfq ZE~ G
51Testing软件测试网pP!Pro~"Q A0l
2、 它的行为一直和我期望的一致吗?51Testing软件测试网8R"Qp[P#f
编写单元测试,如果只测试代码的一条正确路径,让它正确走一遍,并不算是真正的完成。软件开发是一个项复杂的工程,在测试某段代码的行为是否和你的期望一 致时,你需要确认:在任何情况下,这段代码是否都和你的期望一致;譬如参数很可疑、硬盘没有剩余空间、缓冲区溢出、网络掉线的时候。51Testing软件测试网$rf%a0lAtZf
3K;uH*c8pneahn+s03、 我可以依赖单元测试吗?
;qqV"~A0 不能依赖的代码是没有多大用处的。既然单元测试是用来保证代码的正确性,那么单元测试也一定要值得依赖。
V%X%A7B"X6d/X0
GM|'mt[hV04、 单元测试说明我的意图了吗?
`SW,f)A0 单元测试能够帮我们充分了解代码的用法,从效果上而言,单元测试就像是能执行的文档,说明了在你用各种条件调用代码时,你所能期望这段代码完成的功能。51Testing软件测试网7XwS6MF/z3h,h
%xY?x(]II0不写测试的借口
"j/WcV/l0
0qK/WU2m.D*E5vlC051Testing软件测试网9K^^CXvk
到这里,我们已经列举了使用单元测试的种种理由。也许,每个人都同意,是的,该做更多的测试。这种人人同意的事情还多着呢,是的,该多吃蔬菜,该戒烟,该多休息,该多锻炼……这并不意味着我们中的所有人都会这么去做,不是吗?
5ULZC%BJ0
O1U/p7hc01、 编写单元测试太花时间了。51Testing软件测试网!yp)}nW ^O
我们知道,在开发时越早发现BUG,就能节省更多的时间,降低更多的风险。51Testing软件测试网{ ?M l6I5L
下图表摘自<<实用软件度量>>(Capers Jones,McGraw-Hill 1991),它列出了准备测试,执行测试,和修改缺陷所花费的时间(以一个功能点为基准),这些数据显示单元测试的成本效率大约是集成测试的两倍,是系统 测试的三倍(参见条形图)。51Testing软件测试网whn2Z,x0q{c
51Testing软件测试网 Q8h.BBk{ s-J
术语:域测试(Field test)意思是在软件投入使用以后,针对某个领域所作的所有测试活动。
@W4EOBDd G;|p0 如果你仍然认为在编写产品代码的时候,还是没有时间编写测试代码,那么请先考虑下面这些问题:51Testing软件测试网E? bgxhxgv
1)、对于所编写的代码,你在调试上面花了多少时间。51Testing软件测试网-_9m-H9kkY
2)、对于以前你自认为正确的代码,而实际上这些代码却存在重大的bug,你花了多少时间在重新确认这些代码上面。
r\&G