为什么要单元测试

简介: 在互联网时代,DevOps 强调“谁开发谁负责”,单元测试成为保障代码质量的基石。测试金字塔提倡以80%单元测试为根基,提升调试效率、代码可维护性与研发信心,最终实现高效迭代与长期降本增效。

在如今的互联⽹时代,软件迭代的速度越来越快,研发的职责也越来越多。DevOps的理念是"you build it, you run it",研发/测试合⼆为⼀的趋势也可以理解为对"you build it, you test it"的呼吁。当研发要对⾃⼰写的代码质量和测试负责的时候,好的测试实践就必不可少了。

测试⾦字塔

就像盖楼需要从打地基、竖钢筋、灌⽔泥层层往上构建⼀样,测试也有类似的测试⾦字塔架构。下图出⾃《Software Engineering at Google》的测试章节,总结了Google在测试⽅⾯的最佳实践。我们可以看到测试⾦字塔由三层构成,最底层就是单元测试、占⽐80%,是软件系统的地基。再往上是集成测试和端到端测试,分别占15%和5%。因为从下往上占⽐逐层缩减,因此被称为测试⾦字塔(跟盖⾼楼⼀样)。⾕歌推荐的这个⽐例是多年实践出来的结果,意在提升研发的效率(productivity)并提升对产品的信⼼(product confidence)。

测试⾦字塔的核⼼理念之⼀就是“Unit Test First“,每个软件项⽬⾥的第⼀⾏测试应该是单测(TDD甚⾄认为第⼀⾏代码就应该是单测),⽽且⼀个项⽬⾥占⽐最⾼的测试也应该是单测。


优秀的软件离不开单元测试

为什么业界都把单元测试放在这么重要的位置?“抓⼤放⼩”,只写端到端测试不⾹吗?这⾥我们来展开讲讲单测的好处。

提升debug效率

单元测试是软件⼯程极佳的地基,因为它们快速、稳定,并且极⼤地缩⼩了问题范围,提升故障诊断的效率。

  • 测试更快:单测没有其他外部依赖,跑的快,可以提供更快的反馈环,更快的发现并修复问题。
  • 测试更稳定:同样因为0依赖,单测相⽐于其他类型的测试更稳定,不会受外部其他模块的不兼容变更影响。因此单测也是最能带给开发者信⼼的测试类型。
  • 问题更容易定位:单测以最⼩软件单位为边界,出了问题可以缩⼩定位范围。相⽐之下,越是⾦字塔上层的测试类型,定位问题的困难度越⼤。复杂的端到端测试涉及众多的模块,需要⼀⼀排查定位问题。

提升代码质量

代码是写给⼈看的,好的代码应该是易读、易改、易维护的。写单测的过程其实就是吃⾃⼰代码狗粮(dogfood)的过程,从⽤户/研发视⻆去使⽤⾃⼰的代码,帮助我们提升代码质量。

  • 好的代码是易测的:业界很早就提出了圈复杂度(Cyclomatic complexity)的概念,⽤来衡量⼀个模块判定结构的复杂程度,其数量上表现为独⽴路径的条数,也可理解为覆盖所有的可能情况最少使⽤的测试⽤例个数。圈复杂度⼤说明程序代码的判断逻辑复杂,可能质量低,且难于测试和维护。因此好的代码⼀定是圈复杂度低的,也是易于测试的。
  • 易于迭代演进:没有什么软件是⼀成不变的,好的软件系统应该是易于演进的。单测覆盖⾼的项⽬模块更原⼦化,边界更清晰,修改起来更容易。单测覆盖更全的项⽬重构的⻛险也相对更⼩,相反⼀个没有单测覆盖的复杂项⽬是没⼈敢碰的。
  • 更优质的设计:前⾯也提到,好的单测能够提升代码的质量。如果⼀个研发需要给⾃⼰的代码写单测,他就会注重代码的模块化分割,减少过⻓、圈复杂度过⾼的method。下⾯的例⼦就是⼀段没有单测的代码的认知复杂度值(可以理解是圈复杂度的⼀个改良版,从代码是否容易理解的⻆度衡量),超标了⾜⾜三倍。现在回过头来想补单测,脑袋都⼤。


提升总体研发效率

磨⼑不误砍柴⼯,⾼质量、完善的单测可以提升研发质量和效率,加快项⽬总体交付速度。这句话乍⼀看是反常识的,写单测往往⽐写实现逻辑要更耗时,怎么还能提⾼效率?这也是⼤家不写单测最常⻅的理由:“项⽬赶进度,来不及写单测”。如果我们的项⽬⽣命周期是以⽉计算的,写个原型很快就下线了,那写单测的确ROI不⾼。但阿⾥有很多to B的业务,提供给⽤户的能⼒都是以年计算⽣命周期的,⾼质量代码的ROI随着时间推移会越来越⾼,具体体现在以下⽅⾯:

  • 减少debug时间:上⾯提到种种提升debug效率的原因,这⾥不再重复。⼀⽅⾯更⾼的单测覆盖可以节省debug所花费的时间,另⼀⽅⾯有充⾜测试覆盖的项⽬本身bug数量就会更少。举个现实中的例⼦:某团队由于历史上⽋的种种债务,基本全靠端到端测试,毫⽆单元测试覆盖。造成的后果也⾮常严重,团队oncall的同学 > 50%的时间都是在修复各种奇怪的bug,没法投⼊宝贵的精⼒到架构升级等⻓期更重要的项⽬上。
  • 增加代码变更的信⼼:前⾯提到没有测试覆盖的代码没⼈敢碰,有充⾜单测覆盖的代码可以显著提升改造代码的信⼼和意愿。再给⼤家举个例⼦:我加⼊阿⾥之前在Google总部⼯作过将近⼗年。如果你在Google⼯作过就会发现,你的代码经常会收到毫不相关团队成员发起的code change。⼤多数情况下这些都是同学们⾃发的去做⼤⾯积重构(mass refactor),⽐如看你的Java代码没有⽤Builder模式,就会帮你做个重构(Google⾥有⼤量⾃动化⼯具简化这些重构⼯作)。我们抛开主观意愿不谈,如果是没有测试覆盖的代码、还是毫不相关组的,你敢这么重构吗?我们都希望能有像⾕歌那样整洁的代码,但没⼈敢碰的代码怎么变得更好?
  • 提升代码⾃解释性:⽂档能够提升代码的⾃解释性,让研发效率更⾼。好的单测其实也可以被看作代码的⽂档,通过读测试就能快速理解代码的作⽤(参⻅TDD)。单测作为⽂档同时还完美的解决了⽂档保鲜的难题,给开发者提供了⼀套⾼质量、随着代码不断更新的⽂档。
  • 更⾼效的code review:不是所有的问题和设计上的缺陷都能通过静态检查发现,这也是为什么需要⼈⼯code review作为代码质量的最后⼀道防线。在Google,代码评审是代码合并最重要的⼀个环节,因此评审的效率直接影响总体的研发效率。好的单测覆盖能够减轻评审⼈的负担,让他们把精⼒投⼊到更重要的部分(⽐如代码设计)。
  • 更频繁的发版:敏捷开发倡导的持续集成、持续部署的前提就是全⾯、⾼质量的⾃动化测试。敏捷开发对于研发的提效就不多展开了。但光是能够更快速的发版本身就已经⾮常有价值了。
相关文章
|
23天前
|
Java Maven Spring
SpringBoot中如何自定义starter
自定义Spring Boot Starter通常包含两个模块:starter负责依赖管理,autoconfigure实现自动配置。前者引入后者。在autoconfigure中,通过@Configuration和@Bean定义配置类与Bean,并结合@Conditional条件注解控制Bean的创建;最后在META-INF/spring.factories(或Spring Boot 3的xxx.imports文件)中注册自动配置类全名,完成自动化配置。
|
23天前
|
存储 弹性计算 人工智能
大模型应用开发
大模型应用开发需通过API与模型交互,企业可选择开放API、云平台或本地服务器部署。开放API使用便捷但存在安全与成本问题;云部署易维护但仍有隐私风险;本地部署安全性高、长期成本低,适合企业级应用,不建议个人电脑部署。
|
23天前
|
搜索推荐
冒泡排序与其它排序算法比较
冒泡、选择、插入排序时间复杂度均为O(n²)。冒泡稳定,可优化至O(n),交换频繁;选择不稳定,交换次数少;插入稳定,对有序数组高效,三者中交换最少。相较其他高级排序无时间优势。
|
23天前
|
存储 SQL 关系型数据库
什么是回表查询
MySQL中InnoDB引擎的聚簇索引将数据与索引存储在一起,叶子节点存整行数据,每表仅一个;二级索引则分离存储,叶子节点存主键值。回表查询需先查二级索引再查聚簇索引,性能较低。优化方式包括:优先主键查询、使用联合索引实现覆盖索引、利用MySQL 5.6+的索引下推功能,在存储引擎层提前过滤,减少回表次数,提升查询效率。(238字)
|
23天前
|
存储 关系型数据库 MySQL
为什么MySQL索引结构是B+tree
MySQL采用B+树作为索引结构,因其非叶子节点仅存键和指针,使每页可存储更多键,树层级更浅,查询更快。所有数据集中于叶子节点,查询效率稳定,且叶子节点形成双向链表,支持高效范围查询。
|
23天前
|
缓存 安全 Java
Java基础
重载指方法名相同但参数不同,发生在编译期;重写指父子类中同名同参方法,发生在运行期。String不可变,StringBuilder非线程安全,StringBuffer线程安全,适用于多线程场景。
|
23天前
|
存储 算法 NoSQL
Java基础
本文简要介绍了常见数据结构,包括线性结构(如动态数组、链表、栈、队列)、非线性结构(如优先级队列、哈希表、红黑树、跳表、B+树),并列举了Java中的典型实现类,适用于集合类学习与面试参考。
|
23天前
|
前端开发 Java 测试技术
生产环境发布管理
本文介绍大型团队中多环境自动化部署的实践方案。通过CI/CD平台实现从开发(dev)到测试(test)、灰度(pre)再到生产(prod)的全流程管控,涵盖环境职责划分、分支管理、容器化部署及热更新回滚机制,提升发布效率与系统稳定性。
|
23天前
|
负载均衡 算法 搜索推荐
重复性工作中如何培养匠心
本文探讨在互联网低增长周期下,工程师如何在重复性工作中培养匠心。通过分析“结果价值空间、过程贡献度、可预测性”三个指标,指出即便工作看似重复,仍可通过创新、沉淀与自我突破挖掘成长空间,实现从熟练到卓越的跃迁。
|
23天前
|
负载均衡
Eureka服务注册流程
服务提供者启动时向Eureka注册实例信息,定期发送心跳续约,否则被剔除。消费者通过Eureka发现服务,获取可用实例并负载均衡调用。服务下线时,提供者发送取消注册请求,Eureka将其从注册表移除。