单元测试通常是由软件开发⼈员编写和运⾏的⾃动测试,以确保应⽤程序的某个部分(称为“单元”)符合其设计并按预期运⾏。在过程式编程中,⼀个单元可以是⼀个完整的模块,但它更常⻅的是⼀个单独的函数或过程。在⾯向对象的编程中,单元通常是整个接⼝,例如类或单个⽅法。通过⾸先为最⼩的可测试单元编写测试,然后为它们之间的复合⾏为编写测试,可以为复杂的应⽤程序建⽴全⾯的测试。

在开发过程中,软件开发⼈员可能会将标准或已知良好的结果编码到测试中,以验证单元的正确性。在测试⽤例执⾏期间,框架会记录不符合任何条件的测试,并在摘要中报告这些测试。为此,最常⽤的⽅法是测试 - 函数 - 期望值。

好处

单元测试的⽬标是隔离程序的每个部分,并表明各个部分是正确的。单元测试提供了⼀个严格的书⾯契约,你实现的代码逻辑单元必须满⾜这个契约。因此,它提供了⼏个好处。

  • 早期发现问题: 单元测试在开发周期的早期发现问题。这包括程序员实现中的错误和单元规范的缺陷或缺失部分。编写⼀组完整的测试的过程迫使作者仔细考虑输⼊,输出和错误条件,从⽽更清晰地定义单元的所需⾏为。在编码开始之前或⾸次编写代码时查找 Bug 的成本远低于以后检测、识别和更正 Bug 的成本。已发布代码中的错误也可能给软件的最终⽤户带来代价⾼昂的问题。如果写得不好,代码可能不可能或难以进⾏单元测试,因此单元测试可以迫使开发⼈员以更好的⽅式构建函数和对象。

  • 适合敏捷编程:测试驱动开发(TDD)经常⽤于极限编程和Scrum,它要求单元测试是在编写代码本身之前创建的。测试通过后,该代码将被视为已完成。如果单元测试失败,则将其视为已更改代码或测试本身中的 Bug。然后,单元测试允许轻松跟踪故障或故障的位置。由于单元测试在将代码交给测试⼈员或客户端之前会向开发团队发出问题警报,因此在开发过程的早期会发现潜在的问题。

  • 适合回归测试: 单元测试允许程序员在以后重构代码或升级系统库,并确保模块仍然正常⼯作(例如,在回归测试中)。该过程是为所有函数和⽅法编写测试⽤例,以便每当更改导致错误时,都可以快速识别它。单元测试检测可能破坏设计合同的更改。

  • 确保单元稳定:单元测试可以减少单元本身的不确定性,并且可以⽤于⾃下⽽上的测试样式⽅法。通过⾸先测试程序的各个部分,然后测试其各部分的总和,集成测试变得更加容易。

  • 单元测试就是”⽂档”:单元测试提供了⼀种系统的活⽂档。希望了解单元提供哪些功能以及如何使⽤它的开发⼈员可以查看单元测试,以基本了解单元的接⼝ (API)。

局限性和缺点

  • 测试不会捕获程序中的每个错误,因为它⽆法覆盖程序中的每个执⾏路径。

  • 单元测试的详细层次结构不等于集成测试。与外围单元的集成应包含在集成测试中,但不应包含在单元测试中。

  • 软件测试是⼀个组合问题。例如,每个布尔决策语句⾄少需要两个检验:⼀个结果为“true”,另⼀个结果为“false”。因此,对于编写的每⾏代码,程序员通常需要3到5⾏测试代码。这显然需要时间,收益可能和投⼊不成正⽐。

  • 有些场景不容易测试。 例如那些⾮确定性或涉及多个线程的问题。此外,单元测试的代码与它正在测试的代码⼀样可能存在错误。

  • 流程和规范也很重要,规范的制度确保定期审查测试⽤例失败并⽴即解决。如果这样的过程没有被实现并根深蒂固地融⼊到团队的⼯作流程中,应⽤程序将与单元测试套件不同步,增加误报并降低测试套件的有效性。

单元测试最初兴起于敏捷社区。1997年,设计模式四巨头之⼀Erich Gamma(设计模式、Junit、Eclipse、VSCode)和极限编程发明⼈Kent Beck(极限编程、测试驱动开发、JUnit)共同开发了JUnit,⽽JUnit框架在此之后⼜引领了xUnit家族的发展,深刻的影响着单元测试在各种编程语⾔中的普及。当前,单元测试也成了敏捷开发流⾏以来的现代软件开发中必不可少的⼯具之⼀。同时,越来越多的互联⽹⾏业推崇⾃动化测试的概念,作为⾃动化测试的重要组成部分,单元测试是⼀种经济合理的回归测试⼿段,在当前敏捷开发的迭代中⾮常流⾏和需要。

⼆⼗年前,单元测试的⽅法和JUnit引⼊到国内的时候,⼤家都是求知若渴,将其奉为圭臬。不幸的是,在当前⼀个浮躁的追求短期利益的互联⽹时代,很多公司甚⾄头部⼤⼚都已经不把单元测试作为提升软件质量的⼀种⼿段,甚⾄⼏乎不写单元测试了。“能逮着⽼⿏的就是好猫”成了⼤家衡量成功的标准,糙快猛的把原型堆出来,原型试错,快速拿奖⾛⼈⼤⾏其道。万幸的是,在⼀些公司中,还是有⼀些⼯程师,依然坚持着传统的信念,不会把“时间紧需要快速出活”作为理由降低代码质量,还是把单元测试、代码规范等坚持下来。

即使现在有些公司,采⽤tcpcopy、goreplay复制线上流量对测试环境的系统进⾏测试,或者像滴滴的写轮眼可以在⼀定程度上解决⼿⼯编写测试的麻烦以及微服务依赖的问题,但是它们更多的是复制线上流量对集成系统所做的正⾯测试,对于单元测试的全⾯覆盖、尤其是测试路径的全⾯性包括负⾯测试还需要单元测试去实现。当然还有⼀些通过历史数据进⾏单元测试的⼀些⽅法也是类似。

Go语⾔不像Java等程序提供JUint、TestNG这样的单元测试框架,⽽是把单元测试的思想融⼊到Go编程语⾔之中,彻彻底底的将单元测试作为代码规范的第⼀要务,这是很了不起的。就是Go团队本身,在开发Go标准库时,都是有各种单元测试的,如果你想提交对代码逻辑的改动,都需要跑过既有的单元测试,对于新的测试也必须提供⾜够的单元测试。对于⽣成对性能提升的代码,还需要有充分的benchmark代码的⽀撑, ⽐如下⾯字节跳动语⾔团队提供的Go sort算法的提升,提供各种benchmark的性能对⽐,⼤部分场景下性能都有提升(delta负值)。

文档更新时间: 2023-09-19 07:45   作者:Minho