软件匠艺之良好单元测试的必要性

单元测试

什么是单元测试

你可以在互联网上找到众多的定义,以下我引用维基百科的定义和《单元测试艺术》的定义。

单元测试是一段测试代码,它调用软件代码的某个工作单元,并检查该工作单元的一个特定功能和最终执行结果。 如果期望的结果和最终执行的结果不一致,则单元测试就会失败。 一个单元测试的范围可以小到一个方法,也可以大到多个类。 –《单元测试艺术》

单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 — 维基百科

如果在没有单元测试的情况下,程序员通常在写完代码时,需要手动运行程序或者通过调试工具来检验代码功能是否满足期望,引入单元测试后,就是将这部分工作完全自动化,使用代码来测试代码。因此程序员是编写单元测试的第一责任人,因为程序员在编写软件代码时,需要保证代码交付的质量。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。单元测试是对软件设计的最小单位进行正确性检查的测试工作,其测试目的在于发现模块内部存在的各种错误。单元测试的要点是进行单元模块所有数据项的正确性、完善性测试,主要关注模块的算法细节和模块接口间流动的数据。

为什么需要单元测试

想想看我在A公司经历的情况,整个软件系统没有任何自动化测试用例支撑,只是通过部分手动测试验证功能的可用性是远远不够的,这样的软件系统其实是维护人员的噩梦,因为深藏在代码里的bug就像一颗颗定时炸弹,随时都有可能让你的系统崩溃。
通过使用单元测试,软件质量可以达到一下目标:

  • 软件得到充分的测试

    通过单元测试的定义可以看出,单元测试的对象其实是函数或者类,属于软件系统的最小功能单元。由于只关注软件的最小部件,因此测试目标和测试用例比较容易建立,不容易遗漏,测试覆盖会更加充分。

  • 软件的异常分支得到良好的覆盖

    手动测试相比单元测试最大的弱点是无法充分的覆盖异常情况,手动测试更多的只能验证软件的功能,有些异常很难模拟。但是单元测试可以通过使用测试框架,对软件单元外界的异常进行模拟,可以对异常分支进行充分的验证。

  • 测试的自动化保证了软件系统能够得到持续系统验证

    单元测试用例的集合和代码一样,都是软件生命周期中的重要组成部分,当软件系统在不断演进的过程中,单元测试用例的集合可以保证系统功能按照期望的方式运行,新增的代码对原有系统功能没有影响。单元测试用例的集合可以帮助程序员快速的发现新功能对原有功能的冲击和影响。

  • 单元测试帮助程序员尽快的发现问题,而不是隐藏问题

    传统的开发模式下,程序员只关注软件需求和功能的实现,因此软件代码都是一气呵成,聪明的程序员写代码的热情一旦被激发,就会一发不可收拾,当软件主要功能的代码实现后,他们才会去考虑功能及可靠性的测试。在这种开发模式下,一个很大的缺陷是程序员的手动测试用例很难充分覆盖,因此时常会出现漏测,和考虑不周的测试,很多隐藏的异常问题很难被发现。同时,当程序员手动测试发现问题时,调查和解决问题的效率很低,由于新增和修改的代码太多,并且很多代码已经是几天或者数周前写的,此时程序员早已对旧代码的逻辑变得陌生,很难通过阅读代码快速发现问题,必须借助一些调试工具(GDB 等)进行调试。我就有类似的经历,职业之初时常因为一个简单的逻辑和拼写错误耗费数小时更甚于数天的时间来定位。
    TODO: picture for 传统的开发调试模式
    如果程序员采用TDD(即测试驱动开发)开发模式的话,单元测试用例是在开发业务代码之前编写,因此每当你完成部分业务代码,通过运行测试用例,程序员可以快速得到反馈,由于开发测试用例和业务代码几乎是同时进行的,因此当用例失败时,程序员需要定位的代码片段非常小,并且这些代码几乎是在十几分钟之前开发的,程序员清楚的知道自己修改了什么,很多时候只需要通过快速的阅读代码就可以解决问题。
    单元测试的另外一个好处是,当程序员对已有软件系统进行修改时,通过运行单元测试可以得到快速反馈,如果受到影响,则可以快速发现问题,不用推迟到集成测试阶段。并且单元测试发现的问题要比集成测试发现的问题更容易调查和调试。

写出良好的单元测试的必要性

通过我在B公司X团队经历证明,通过覆盖充分的测试用例并不一定能在软件生命周期中带来正向反馈,相反,那些随着时间推移逐渐腐臭的测试用例代码,反而成了程序员的噩梦,这样的用例无法在保证产品质量上带来任何好处。实践证明,通过覆盖率单纯的来衡量单元测试质量会适得其反,聪明的程序员们常常会投机取巧,为了满足覆盖率而做一些对质量毫无意义的事情,恰恰是这样的作法,使得单元测试的质量大打折扣,随着时间的推移变成了累赘。

单元测试诚然是保证代码质量的一项关键技术,但是程序员首先要认识到,单元测试代码的质量和产品代码的质量同样重要。其次需要掌握生产良好单元测试代码的相关技术。

我可以列举一些糟糕单元测试的例子,以及他们的副作用:

  • 糟糕的可读性
  • 单个测试用例测试多个场景
  • 用例执行慢
  • 测试用例依赖于外部环境
  • 过多的重复代码

良好的单元测试具有以下一些属性:

  • 能够完全自动化;
  • 完全控制所有正在运行的部分(在需要时使用模拟或存根来实现这种隔离);
  • 如果是许多其他测试的一部分,可以按任何顺序运行;
  • 在内存中运行(例如,无 DB 或文件访问);
  • 始终返回相同的结果(例如,您始终运行相同的测试,因此没有随机数。保存那些用于集成或范围测试);
  • 运行快(单个用例执行只需要几毫秒);
  • 测试系统中的单个逻辑概念;
  • 良好的可读性;
  • 良好的可维护性;
  • 值得信赖(当你看到它的结果时,你不需要为了确定而调试代码);

质量在于充分完备的测试,尽早发现问题

质量在于充分完备的测试,尽早发现问题

  任何软件产品,在交付给客户或者用户之前,必须经过严格并且充分的测试,由此才能保证软件产品功能可用,易用,并且能够可靠持续的提供服务。道理很简单,只有稳定可靠的软件服务才能在用户或者客户群里赢得良好的声誉,从而赚取更多的利润。相反随时崩溃的程序带给用户的只有极其差劲的体验,我想说只有傻瓜才愿意为这样的软件服务买单。如果你的软件面对的不是直接用户,而是中间的供应商,那么你就更应该保证软件的质量,在这种情况下,任何软件的bug都可能给你的客户带来极大的损失,你的公司也会因此丢失这样的客户,同时使你在同样的客户群中失去信誉,因此作为软件提供商,必须清醒的知道,应该用高质量的软件来成就客户才能赢得双赢的局面。

  软件测试如果按照测试阶段分类的话,主要分为单元测试、集成测试、系统测试和验收测试。其中单元测试按照测试方式的话,属于白盒测试,是程序员在写代码的过程中必须完成的测试,距离编码最近,是能够最早发现程序问题的一种测试手段。由于单元测试能够帮助程序员在编码的早期阶段发现问题,程序员可以快速定位到问题的根因,因此解决问题的成本几乎可以忽略不记。

  单元测试是保证程序质量的第一步,因此程序员对此必须要有清醒的认知,业务代码和单元测试的代码具有相同的重要性。

  在这里,我想用我的个人职业生涯的经历来说明,怎么做才是提高软件质量的正确做法。

  我硕士毕业后的第一份工作是在A公司,正是在A公司,奠定了我十多年软件职业的基础,但是在A公司工作的三年里,我甚至没有听过关于单元测试的任何讨论,我个人甚至没有接触单元测试的概念,在A公司,程序员们按照需求编写相应的软件功能,然后直接交给测试同事,由于编写的软件是windows界面,测试的同事们也不了解什么是自动化测试,以我现在的目光来看,在A公司,测试同事们使用最原始的方式进行测试,不借助任何工具。而程序员们只关注编码,测试并不是自己的责任。当你看到这里,也许你很好奇,以这样的方式开发的软件是否真的可靠?我的直观感受是,我们整个团队,其实对软件的质量并没有十足的自信,因为每次客户在使用我们的软件系统时,都要求我们有足够的技术团队的现场支持,我甚至好几次碰到过在客户现场,团队惊慌失措的解决临时问题,也许这问题就是个低级错误。

  我刚进A公司的时候,我曾经作为技术支持接手过一套软硬件系统,原作者在我进公司的不久前离职了,他是大家公认的牛人,写这套软件的时候据说是起早贪黑在很短的时间内快速完成了需求任务。提到这位同事的时候,公司的领导和其他同事都会眼前一亮,说他在写程序方面非常厉害,佩服的五体投地。而我一个新手可就没那么容易了,当我阅读他写的代码时,满篇的代码都是简写,简短的命名,没有任何注释,深度的嵌套,以及错综的分支,这样的代码很难说具有可读性。为了读懂和理解代码逻辑,我不得不找到硬件的开发文档,同时找了一台设备,自己手动测试加单步调试。我不得不说,这是一套复杂而脆弱的系统,因为几乎没有任何测试就交付给客户使用了。很多次我都是在现场解决一些奇怪的问题同时被一些客户破口大骂。对这样的程序员,我曾有过一丝的崇拜,可是当我在软件工程这条道路上逐渐成熟的时候,我意识到应该对聪明的程序员和专业的程序员加以区分,我觉得我的这位前辈只能归为聪明的程序员,而我更喜欢后者,坚持让自己逐渐成长为专业的程序员。

  当我意识到在A公司成长有限时,我毅然决定离开,选择了远在其他城市的B公司,B公司是国内比较大通信及软件供应商,具有完整的质量管理体系,领导层也具有远见卓识,是国内数一数二的技术型企业,正是在B公司工作的很多年里,让我深入接触了Linux 平台,在软件开发领域打开新的眼界,我的技术能力也得到了快速的发展。正是在B公司,我开始接触到单元测试和TDD的概念,同时我也开始听到了周围一些同事对单元测试的讨论。可是遗憾的是,我现在看来,我们当时的团队X在采用TDD开发模式上是失败的。团队在一开始软件开发时,就设定了单元测试覆盖率需要达到95%以上,但是由于团队成员资历尚浅,缺乏资深专家的正确引导,并没有严格的按照TDD 所谓的先写测试,后写业务代码的流程做开发,大部分的人还是先写业务逻辑的代码,然后再考虑UT覆盖,甚至很多人只是为了满足覆盖率,让用例蒙混过关。我身边所有的同事都认为,单元测试的代码是用来测试的,所以单元测试的代码质量并不重要,这导致团队开发人员对于如何写好测试用例根本没有认知。最后导致的结果是,单元测试没有帮助程序员有效的发现问题,反而成了项目维护的累赘。最典型的问题是当后期代码有任何变动的时候,导致很多用例失败,程序员在维护测试用例上花了很多成本。

  有趣的是,X团队里,起初大家认可单元测试的有效性,可是到后期就开始蔓延单元测试无用论。

  当我从B公司的X团队转到Y团队时,由于Y团队基于开源软件做开发,因为开源软件本身没有引入单元测试框架,因此大家写代码的时候索性都裸奔了,只是在程序员交付之前必须有手动的验收测试报告,程序员在设计软件过程中,需要设计自己的验收测试用例,然后手动执行,交付功能给测试人员时,需要保证验收测试用例全部通过。测试人员会再此基础上再做更加完备的测试。这套流程看似是严谨的,可是常常出现的情况是,程序员的验收测试用例时常设计的比较简单,为了应付交付的压力,验收测试用例基本上只保证了功能的可用,对于异常,性能和稳定性上缺乏足够的测试。同时还存在一个巨大漏洞是,当软件发生维护和变动时,因为缺乏单元测试的覆盖,对于那些解决老问题,引入的新问题很难快速发现,常常漏到集成测试,或者产品验证阶段才发现,这样导致的结果是问题不能尽早发现,因此解决问题的成本会非常高。

  B公司的一大特点是崇尚加班文化,996几乎成了程序员职业的代名词,我当时所在的Y团队成员几乎每天晚上要工作到10点,想想看,高强度的工作再加代码裸奔,软件的质量该如何保证?在我看来,这其实导致一种恶性循环,程序员在疲惫的情况下交付不可靠的软件->测试不充分->疲惫的测试人员漏测->下游或者客户发现问题->程序员加班解决问题。虽然在B公司,我在技术能力上有了很大的突破,可是在我看来,程序员几乎被绑在了公司运转的车轮上,生活空间被工作挤压,同时没有业余的时间研究新技术,学习新的软件工程方法,日复一日使用老旧的技术开发。团队里所谓的技术大牛,在我看来其实都是些聪明的程序员,他们在如何提高软件质量方面,眼界永远越不过自己的鼻尖。因此基层的程序员缺乏专业程序员的引导,他们的知识体系很难得到更新,很多人其实在如何保证软件质量方面没有任何思考,日复一日的拷贝粘贴,或者写出那些看似高明的复杂代码。为什么国内把程序员叫码农,是因为程序员几乎成了一种体力劳动者。

  我所经历的这种情况几乎成了国内技术公司普遍存在的现象。

本文系本站原创文章,著作版权属于作者,未经允许不得转载,如需转载或引用请注明出处或者联系作者。