小样邂逅单元测试后的反思

作者:cathycheng

团队:移动品质中心TMQ

导语

如果你和我一样对单元测试感兴趣,如果你对单元测试的重要性有困惑?如果你对如何开展单元测试无所适从?如果你和我一样,在互联网的今天要求测试全栈,感受压力山大,又想要快速上手单元测试,那么本文可以帮助到你。通过这段时间对单元测试的学习,结合自己在项目内部实施单元测试得到的一些启发,期望本文可以抛砖引玉,为我们系统地理解单元测试提供可借鉴的信息。

本文首先从理论层面对单测进行理解,包括澄清自己对单测的误解以及解惑单测的意义(既然要开搞,必须要真正认同并系统认识它);接着结合自己的实际工作,阐述了单测是如何开展的。本文所涉及的内容,仅仅是个人的片面见解哈,如有表述欠妥的地方,欢迎指正,谢谢!

一、一起来认识单元测试

从不同的角度,测试有着不同的分类。而从研发过程(主要强调发布前的研发过程)来划分,主要包括单元测试、集成测试、系统测试、回归测试。这4种测试是软件全生命周期持续不断的事情,并不是一个阶段性的事情。尤其要注意的是,这4种测试都有自己的侧重点,如下图所示:

从上图可见,单元测试是软件测试中第一个测试阶段,是软件测试的基础。因此,单元测试的效果会直接影响到软件的后期测试,最终在很大程度上影响到产品的质量。今天就来详细聊聊单元测试。下面分别从单元测试的概念,单元测试的理解误区,单元测试的意义三个维度来阐述下单元测试。

1、单元测试的概念

教科书式单元测试的定义是:单元测试是对程序代码单元进行函数级别的测试,是面向最小软件设计单元的验证工作。它是软件最小组成单位的测试,是软件开发过程中的最基本的测试。它处在软件开发过程中实施的最低级别的测试活动,即检查单元程序模块有无错误。它是在编码完成后必须进行的测试工作,也可以称之为模块测试。

于我而言,单元测试不仅仅是写单测代码。实际上,它的手段是多样化的:你可以通过现成的工具检查单元是否正确,可以通过人工review检查单元是否正确,当然你也可以编写测试代码来检查单元是否正确,等等。这些方法,我觉得可以统称为单元测试。在实际项目实践中,我们可以灵活使用这些方法。关于这点,在后面的单元测试策略里面也会提到。

在传统的应用中,单元测试集中在最小的可编译程序单位——子程序(如模块、子例程、进程);在面向对象软件中,最小的可测试单位是封装的类或对象。它的目的在于检验每个软件单元能否正确地实现其功能,满足其性能和接口要求等。

2、单元测试的误区

很多人对单元测试的执行存在误区,包括我自己。常见的单测误区可以归结为以下5种:

第一,浪费的时间太多;

一旦编码完成,开发同学就会迫不及待地进行集成工作,而实际上系统能正常工作的可能性很小,更多情况是充满了各种Bug。这些Bug包含在独立的单元里,其本身也许很微小,但在集成后去修复它却会加倍开销。在google等大型公司,每写一行程序,都可能要测试很多遍。由此可见单元测试是应该受到重视的,绝对不能认为这是在浪费时间。

第二,软件开发人员不应参与单元测试;

理论上,单元测试需要和编码同步进行,即每完成一个模块就应进行单元测试,确保其能实现相应的行为或功能。在对每个模块进行单元测试时,我们不能完全认为其单元独立,它极有可能和其他模块存在直接或间接的逻辑上的关系。若仅由测试人员进行单元测试,往往周期长,耗费大,事倍功半。因此,站在测试人员的角度,我们鼓励开发同学担负起程序的单元测试,在测试同学的辅助下,争取事半功倍。

第三,我是很NB的码农,不需要进行单元测试;

如果我们真正很NB,就应当不会写出bug,但这只是一个神话。缺乏测试(包括开发自测)的代码可能包含许多Bug,甚至因为修复bug而引入新的Bug,如此便会恶性循环。为避免产生恶性循环,代码必须有一张安全网来保护,随时进行的单元测试就是这张安全网。

第四,不管怎样,集成测试或验收测试将会抓住所有的Bug;

集成测试的目标是把通过单元测试的模块拿来,构造一个在设计中所描述的程序结构,通过测试发现和接口有关的问题。在实际项目中,我们或多或少遇到过提测后的软件不可测,有的甚至导致系统崩溃或是死机。回归测试时又发现新的问题,使得测试工作很难开展或进度缓慢。最后只能忍痛加班,得不偿失。这种不通过单元测试将所有模块预先结合在一起,作为一个整体来进行测试,过程往往是苦不堪言。

第五,单元测试效率不高;

实践证明,缺陷发生到被发现之间的时间和发现到改正该缺陷的成本是指数关系。频繁的单元测试能使开发人员排错的范围缩得很小,大大节约排错所需的时间,同时错误尽可能早的被发现和消灭会减少由于错误而引起的连锁反应。在《实用软件度量》(CapersJones,McGraw—Hill1991)一书中,有数据显示,针对某一功能点上进行准备测试、执行测试和修改缺陷的时间,单元测试阶段的成本效率大约是集成测试的2倍,系统测试的3倍(如下图所示)。

3、单元测试的意义

单元测试非常重要。有研究证明,单元测试可以发现整个软件开发过程中的15.75%的缺陷,其价值不容小觑。而下图也从四个方面凸显了单元测试的重要性。

单元测试要写代码,因此不论效率高低,手速快慢,总是会占时间的。所以如果只是考量开发时间的话,开发的时间肯定会延长。但是如果把考量的时间长度延长到开发-自测-测试-上线-线上反馈-bugfix,那单元测试带来效率方面的益处就能体现出来了。另外,个人认为:单元测试是构筑产品质量的基石。我们不要为了节约时间放弃单元测试,这会在后期花费加倍的时间来弥补。尤其是,任何软件开发团队都不愿意因为节约了早期单元测试的时间,而导致开发的整个产品失败或重来。

二、如何开展单元测试

上面说了很多单元测试的好,那单元测试方便开展吗?该何时开展呢?本质上,单元测试是针对代码进行的测试,其工作量和难度都比较大。在时间上,单元测试的开展是越早越好,应该与编码同时进行,最好在提测之前完成。PerRuneson教授利用Zachman’sFramework理论,发起了一个关于单元测试实践的调查。调查对象来自于全球12个公司里面的15个代表,调查的主题围绕”单元测试的定义,应用单测的优势和劣势(困难)”展开,最后归纳得到了一些结论。我就直接窃取他的结论(如下表),希望对我们有一些参考意义。(关于调查详情,参考文献:《A survey of unittesting practices》)。

注:Zachman’s Framework理论是从what,how,where,who,when,andwhy6个方向进行问题分析。

对表格内容的简单归纳如下:

(1)单测的定义:他们认为单测是针对最小单元或单元集进行的结构化测试,而且单测最好以自动化形式呈现,由开发同学来主导实现,可以针对整个solution展开;单测必须要快速运行,并及时查看结果,保障被测功能的正确性。

(2)开展单测的优势:单测开展后,识别系统的单元便于理解单元的功能细节,有助于我们深刻地理解系统各个单元间的逻辑关系、时序关联以及功能依赖。而且,单测运行在整个系统环境下,可以快速发现其它模块的变化。目前也存在一系列的单测自动化框架,它们方便单测的展开,特别是遇到连续的回归测试时,自动化的单测收益更明显。另外,部分商业安全要求的软件对安全标准有要求,也需要开展单测。而针对敏捷开发,单测用例集可以作为技术说明文档沉淀。

(3)开展单测的困难:针对GUI测试,单元不独立、大数据结构的被测对象展开单元测试非常困难(所以我们尽量选择逻辑层、比较独立的单元进行单测)。如果单测的代码量很庞大,后续的维护也很困难。

需要说明的是,单测实现需要涉及文档化的工作,而且框架不是直接拿来就可以用的,可能需要适当的裁剪。在回归测试阶段,单测用例集的选择没有明确的方法论。在单测的效果度量方面,目前还没有合适的模型。瓶颈点在于,单测对测试人员的能力要求非常高,有时候只能是开发自己决策。

此外,单测做到何种程度可以停止也没有明确的或者可借鉴的标准,这需要公司内部针对不同的产品进行测试积累和经验沉淀。

还有就是,单测的投入成本和带来的收益无法估算或量化。这些都是开展单测可能面临的困难,也是单测缺少驱动力的原因吧。

1、单元测试开展六步法

开展单元测试有它的优势,也有它的劣势。那么在实际的项目中,应该怎样比较恰当地开展单元测试呢?如果你所在的团队准备要开搞单元测试了,下图是根据我的理解总结了六个主要步骤:

第一步,环境准备;

1)单元测试必须要有自己的自动化框架支撑。公司内部推荐使用GmockPlus。

2)必须和开发确认好单测代码的管理方式。有两种方法。方法一:新拉一条分支线管理,和开发线代码保持同步;方法二:与开发共用一条代码线,新增加工程的方式。各团队需要根据自己的团队特色进行合理的选择。但是,不论选取哪种方法都需要新增加一个编译选项,专门用于单测代码的编译。

3)优先确认好助力分析的静态工具。工具能够导出程序的控制流程图,给出程序环路复杂性(如McCabe复杂性度量等);能够输出单元模块的组成和相互间的调用关系;能够生成单元结构的控制流程图。推荐使用EA\UnderStand\SourceMonitor。

第二步,开发输出详细设计文档;

单元测试的主要依据是详细设计文档。单元测试需要从程序的内部结构出发设计测试用例。因此,开发同学在进行编码之前一定要树立单元测试意识,输出有指导意义的详细设计说明书(这里受限于我们的敏捷迭代,在实际操作中,可以口头沟通以list的形式,不强制要求专业的文档化),对被测对象的可测性需要重点考虑,便于单测人员确定重点测试单元并针对性设计单测用例。

第三步,选择单测测试对象;

工作中,我们一般采用测试需求驱动测试方法,需要对重用性高、调用频繁或核心功能的单元模块优先选择。根据八二法则,80%的代码错误,可能存在于20%的代码中;这20%,就是算法密集度高的代码,也就是功能逻辑复杂的代码。我们选择具备这些特色的代码作为单测的对象。

第四步,设计单元测试策略;

在测试过程中,我们应该灵活运用工具代码走查、人工代码走查和功能测试这三种方法。它们的有效组合能提高测试效率,避免很多重复的工作,从而减少测试工作量。尤其在测试资源极为有限的情况下,这种方法的性价比高,可以达到较好的测试效果。

运用工具代码走查方法中,白盒测试工具对代码进行检查是一种代价很低的测试方法。在选定要检查的规范后,整个检查过程只需几分钟。我们使用了Klocwork以及自研的一个小工具NewSpecialCodeAnalyze(该工具专门针对代码书写或使用的windowsAPI不规范进行检查,挖掘可能存在的资源泄漏,感兴趣的同学可以联系zhenyuqiao提供技术支持)。

人工对代码进行走查,可以静态走查,也可以动态走查(调试)。人工走查主要依赖于个人技术和经验,建议成立走查小组来覆盖开发和测试同学。走查发起人可以是当前迭代的开发负责人。他/她负责对走查会议进行良好的策划和组织,不需花费太多的时间,但可以达到很好的效果。

功能测试是代价最高的测试方式。它可能面临多次回归测试,周期较长,而且需要测试人员编写维护测试驱动程序和桩程序,对测试人员的编程能力也有一定的要求。除此之外,它对测试用例的设计和维护也是一项很繁琐的工作。因此,功能测试需要和前两种方式搭配好。

第五步,设计单元测试用例&编码;

单元测试可以从单元功能、单元接口、数据结构、语句/分支覆盖等维度进行单元函数测试。对单元功能的测试是保证单元模块具有完成符合设计要求的功能;对单元接口的测试是保证在测试时进出程序单元的数据流正确;对数据结构的测试是保证存储的全局数据、局部数据在算法执行的过程中的完整性;对语句分支覆盖的测试是保证单元函数在极限边界条件能够正确执行函数的每条语句和每个分支,消除无用代码。单元函数是由各种语句组成的程序代码,对各种语句测试用例的设计是单元测试的关键。关于单元测试用例的设计,在2.2小节会进行详细的介绍。

编码工作是在设计好单元测试用例后立即开展的工作。理论上开发同学完成一个函数的编写,对应的单元测试也应该准备就绪,这样才能发挥单测的最大效果。但在实际操作过程中,我们期望单元测试的编码工作需要在整体功能提测之前完成。

第六步,单测效果验收;

度量单元测试的效果收益目前业界还没有一个公认的ROI模型。结合我们的项目实践,这里的R可以参考花费时间,I可以参考覆盖率贡献度、迭代周期的长度、测试独占时间的占比、提测后千行代码的缺陷率以及线上质量(包括crash和用户反馈)等。这些数据之间的关联模型目前还没有。单测过程采用覆盖率工具,这个是毋庸置疑的,否则用例执行后无法对被测对象做进一步的分析。推荐C++Validator(免费,且操作简单),桌管内部的damocles(cc.oa.com)也是可以的,目前正在内测阶段,届时上线也欢迎使用。

2、单元测试用例设计方法

在实际开发中,每个编写代码的人都自觉或不自觉、或多或少做过所谓“单元测试”,如编码规范、逻辑功能检查、编译查错和调试等,但是这些还不能算严格意义上完整的单元测试。无序或无组织的所谓“单元测试”,容易造成对单元测试认识的偏差,难于提高软件单元的质量。单元测试到底该该如何设计?明确单元测试的测试内容和范围,这是单元测试的基本要求。

进行有计划的单元测试,应根据需求和详细设计文档的要求。如下图所示的8类测试项将是我们在实施单测时需要考虑的内容:

在实际项目实践中,使用者可以根据自己的项目复杂度以及需求,挑选其中的进行参考。

三、单元测试实践

前面两部分洋洋洒洒说理论,终于把我认为的单测里面几个核心的要点讲完了,也算是对单元测试有了基础宏观的理解。这部分就是理论到实践了。在拿到被测对象后,我将重点介绍如何选择单测对象,以及如何设计自己的单测用例。

1、单测对象的选择

按照我们2_1描述的方法,尽量利用工具辅助我们分析。

首先,利用EA工具得到单元模块间的关系。我们的工程比较简单,该功能的重点类是C**Exe和C**ovider,因此主要是针对这两个类进行单测(且这两个类是本次修改的)。

其次,利用SourceMonitor代码度量工具对目标模块进行了度量。按照圈复杂度进行排序,我们得到并选择圈复杂度高的文件,如下图:

C**Exe和C**ovider的复杂度分别是7和17。需要说明的是,上图有其它复杂度高的,但不是本次需求变更的范围。本例中,我们重点进行差异测试。将上面两个类详细展开看,如下图(由于篇幅原因,仅截取部分内容):

接着,我们选择圈复杂度4以上的函数进行筛选。依据重要性、重用性、和可测性选择待测对象。

最后,邀请开发同学,发起单测对象评审,确定被测函数对象。

2、单测用例的设计

对单测函数列表的函数分优先级,逐个进行单测用例设计。这里以C**vider::On***Pos()函数为例。该函数的功能主要用于实现添加桌面快捷方式位置的监控。根据该函数的程序流程图如下(使用understand工具绘出):

针对该单个函数,我们使用了基本路径、判断条件、数据划分和边界值四种基本的方法进行用例设计:

1)基本路径:按照逻辑结构的路径分支进行覆盖,得到如下的6条case:

第一条:(1)-(2);

第二条:(3)-(4);

第三条:(3)-(5);

第四条:(3)-(6)-(7);

第五条:(3)-(6)-(8);

第六条:(3)-(6)-(9);

2)判定条件:对复合(两个以上)的判定条件进行用例设计,!szIcoName || szIcoName0 == L'\0' || !szExePath || szExePath0 ==L'\0' ||!sExeArgs || sExeArgs0 ==L'\0',使用判断条件的用例设计方法覆盖所有条件,得到如下的7条case:

第一条:szIcoName为空指针,szExePath和sExeArgs均非空;

第二条:szIcoName为空串,szExePath和sExeArgs均非空;

第三条:szIcoName非空,szExePath为空指针,sExeArgs非空;

第四条:szIcoName非空,szExePath为空串,sExeArgs非空;

第五条:szIcoName和szExePath非空, sExeArgs为空指针;

第六条:szIcoName和szExePath非空, sExeArgs为空串;

第七条:szIcoName,szExePath和sExeArgs均非空。

3)数据划分:也就是使用等价类划分法对输入数据进行划分(上面的case已经覆盖),有效字符串和无效字符串划分。

4)边界值:不涉及。

由此可见,针对C**vider::On***Pos()函数可以设计11条case来覆盖(基本路径和判定条件存在交叉)。

四、总结

可能大家会有疑问,最开始不是说单测由开发同学来实现最合适吗,怎么最后还是测试同学来主导了?但是现实总是很残酷,这个推行起来的确困难重重。正所谓,己所不欲勿施于人。新政策推行前只能先推己,再及人。我们选择奉行这个观点。只要是为了把质量做的更好,我们多做一点又有何妨?再者,面对严峻的行业形势,这正是一个很好的机会来提升我们的能力,我又何乐而不为呢?

从单元测试的效率角度来考虑,开发人员的知识结构、对代码的熟悉程度,这两方面他们都具有一定的优势;而从单元测试效果的角度考虑,测试人员又具备了他的天然优势。首先,从目前我国实际现状来看,测试人员质量意识要高于开发人员,测试人员参与单元测试能够提高测试质量;其次,对被测系统越了解,测试才有可能越深入,测试人员参与单元测试,将使得测试人员能够从代码层面熟悉被测系统,这对测试组后期集成测试和系统测试活动非常有帮助,会很大的提升集成测试和系统测试质量。因此,单元测试这件事情不能绝对地说由哪种人来做。一种比较被认同的观点是:在允许条件下,由测试和开发共同来做,测试负责制定规范、培训并检查测试效果,开发负责具体的实施,最好是边开发边测试。如果能达到这样的配合,应该是最能实现单元测试的效果与效率上较好的综合和平衡。

总的来说,单元测试应该不仅仅是白盒测试,可以结合黑盒、灰盒,利用静态、动态结合。不仅仅是人工执行,也需要工具和自动化,未来期望可以可视化自动生成单测用例。据说Visual Unit,简称VU,就是一款可视化的C/C++单元测试工具,还没来得及用,抽空可以试一下。此外,单元测试收益的模型也将是后续的研究方向。

综上,文章主要阐述了单元测试的重要性,以及如何比较正确地在项目组内部开展单元测试,所述内容也只是单元测试的冰山一角。后面还需要更多的实践来丰富自己的认知,期待有大牛们的指导!

单元测试的要领你get到了吗?

获取更多测试干货,请搜索微信公众号:腾讯移动品质中心TMQ!

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏phodal

荐书《遗留系统:重建实战》:当你面对一坨代码时,你应该这么做

大多数开发人员的主要时间都是花费在与现有的软件打交道上,而不是编写全新的应用程序。 这就意味着,我经常要遇到很多我写的 shit 一样的代码,你经常要遇到很多你...

3464
来自专栏芋道源码1024

Dubbo源码解析 —— 逻辑层设计之服务降级

前言 在dubbo服务暴露系列完结之后,按计划来说是应该要开启dubbo服务引用的讲解.但是现在到了年尾,一些朋友也和我谈起了明年跳槽的事.跳槽这件事,无非也就...

3438
来自专栏web前端教室

前端开发与数学

image.png 前端开发一般只是操作一些DOM,请求一些JSON,重绘一些DOM,处理一些缓存,触发一些事件,有什么难的?值那么多钱?二十K,三十三K?前端...

2126
来自专栏Android开发实战

Python自学之路

‘’坚持不是一件容易的事情,兴趣是最好的老师‘’,等你坚持过后你总会这么对别人侃侃而谈。

1123
来自专栏较真的前端

[译] 响应式脑电波 — 如何使用 RxJS、Angular、Web 蓝牙以及脑电波头戴设备来让我们的大脑做一些更酷的事

1988
来自专栏java一日一条

为什么开源可以提高程序员的编程技能?

我已经写了很多年的软件。最近我意识到,我越涉及(致力于,结合于等)开源技术,我写出来的代码就更好。这不由地让我疑惑起来:难道里面有什么相关性或因果关系吗?

723
来自专栏AI科技大本营的专栏

经验 | 如何成为一名顶级战斗力的数据分析师?

翻译 | AI科技大本营(rgznai100) 参与 | reason_W 不知道大家以前听没听说过“10x Developer”这个词,如果你连听都还没听说过...

2817
来自专栏大数据钻研

为什么开源可以提高程序员的编程技能?

我已经写了很多年的软件。最近我意识到,我越涉及(致力于,结合于等)开源技术,我写出来的代码就更好。这不由地让我疑惑起来:难道里面有什么相关性或因果关系吗? ? ...

32110
来自专栏PHP在线

为什么开源可以提高程序员的编程技能?

我已经写了很多年的软件。最近我意识到,我越涉及(致力于,结合于等)开源技术,我写出来的代码就更好。这不由地让我疑惑起来:难道里面有什么相关性或因果关系吗? 阅读...

3327
来自专栏FD的专栏

程序员怎样才能写出一篇好的博客或者技术文章

文章来源于 @justjavac在知乎上的邀请,要写在知乎上的回答。因为有原创,所以先首发,免得被伪原创。每天有大把的时间刷GitHub,写博客。从我大二的时候...

742

扫码关注云+社区