为什么要单元测试

简介: 本文探讨单元测试在软件开发中的核心价值,打破“写单测费时误事”的误区。通过解析测试体系演进、测试金字塔模型,阐明单元测试如何提升代码质量、调试效率与团队协作,并揭示常见反模式与认知误区,倡导研发自测、夯实基础,让软件开发从“爬行”迈向“奔跑”。

前⾔

刹⻋是降低了⻋速还是提升了⻋速?我们通常认为写单测费⼒耗时、耽误研发进度,仿佛在给项⽬“踩刹⻋”。⼤家不妨带着这个问题往下看,详细聊聊为什么单元测试可以让软件开发跑得更快。

什么是单元测试

⼤家对于单测应该并不陌⽣,截取⼀段维基百科的定义帮⼤家唤醒⼀下记忆:

在计算机编程中,单元测试(Unit Testing)⼜称为模块测试,是针对程序模块(软件设计的最⼩单位)来进⾏正确性检验的测试⼯作。

单元测试的理念其实⼀直是编程的⼀部分。我们第⼀次编写计算机程序时,肯定会输⼊⼀些样本数据,查看其是否按照你的期望执⾏。如果结果不符合预期,你肯定在代码⾥穿插过⼤量的System.out.println,确保每个原⼦节点都符合预期。这个过程其实就是把复杂问题拆解成原⼦化的问题、逐⼀攻破的过程。单元测试的⽬的也⼀样,是保障软件程序中每个最⼩单位的正确性,从⽽保障由最⼩单位构建起来的复杂系统的正确性。

深⼊展开单元测试的必要性之前,我们先去考考古,看⼀下测试体系是如何演进的。

测试体系的演进

过去的很⻓⼀段时间⾥,软件测试⼤量依赖⼈⼯检测。软件测试甚⾄是⼀个独⽴的⼯种(QA、Tester),QA/tester的⽇常任务就是进⾏⼤量的⼿⼯测试、繁琐易错。

⾃2000年代初以来,软件⾏业的测试⽅法已经发⽣了巨⼤的变化。为了应对现代软件系统的规模和复杂性,业界演变出了开发⼈员驱动的⾃动化测试实践。我们终于可以摆脱⼿动测试的繁琐,⽤软件来测试软件。但过去的实践仍然留下了深远的影响,软件测试还是⼀个独⽴的⼯种,过去的QA演进成了SDET(Software Developer Engineer in Test),我们虽然进化到会使⽤⼯具了,但我们还只是会⽤⼯具的猴⼦。为什么这么讲?因为这种研发/测试分离的模式本身就留下了很多问题。当研发和测试是两个岗位时,交付的边界是软件整体的功能性(functional requirements)和可⽤性。研发只要保证软件整体上功能完备、可⽤就⾏,测试也会聚焦在集成测试和端到端测试上。但软件是由⽆数个最⼩单位构成的,在这种体系下⼈们会忽视最⼩单位的质量、是否可读可测可演进,最终难免“⾦⽟其外,败絮其中”。

基于种种弊端,⾕歌、微软这些对研发质量⾮常重视的公司都在从SDET的2.0时代过渡到 all-in-one 的3.0时代:微软在2015年去掉SDET⼯种,在陆奇带领的Bing中率先提出“combined engineering” 的概念;⾕歌也将SETI替换成EngProd(Engineering Productivity),专⻔负责测试平台和⼯具的搭建,不负责具体的业务逻辑测试。

为什么需要单元测试

在如今的互联⽹时代,软件迭代的速度越来越快,研发的职责也越来越多。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甚⾄认为第⼀⾏代码就应该是单测),⽽且⼀个项⽬⾥占⽐最⾼的测试也应该是单测。

图⽚来源:Software Engineering at Google

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

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

提升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,代码评审是代码合并最重要的⼀个环节,因此评审的效率直接影响总体的研发效率。好的单测覆盖能够减轻评审⼈的负担,让他们把精⼒投⼊到更重要的部分(⽐如代码设计)。
  • 更频繁的发版:敏捷开发倡导的持续集成、持续部署的前提就是全⾯、⾼质量的⾃动化测试。敏捷开发对于研发的提效就不多展开了。但光是能够更快速的发版本身就已经⾮常有价值了。

反⾯模式和常⻅误区

上⾯提到了写单元测试的种种好处和业界的最佳实践。我们也列举⼀下常⻅的反⾯模式和误区,帮助⼤家更好的规避类似错误。

测试的反⾯模式(anti-pattern)

反⾯模式⼀:冰激凌筒模式

只关注⽤户视⻆的端到端测试、⼤量依赖QA测试都会产⽣如下图所示的反⾯模式。很不幸,这也是在过去的测试体系影响下最常⻅的模式。冰激凌筒模式下,测试套件通常运⾏缓慢、不可靠、难以使⽤。缺失底层的单测也会让项⽬变得⾮常难维护,很难做⼤的改动。

图⽚来源:Software Engineering at Google

反⾯模式⼆:沙漏模式

沙漏模式下,项⽬中有⼤量的单元测试和端到端测试,但缺乏集成测试。虽然它不像冰激凌筒那么糟糕,但仍会导致许多端到端测试失败,这些失败本可以通过⼀套中等范围的测试更快更容易地捕捉到。当模块间紧密耦合,使得依赖项很难单独实例化出来的时候,就会出现沙漏模式。

图⽚来源:Software Engineering at Google

测试的常⻅误区

常⻅误区⼀:⽤户第⼀,测试覆盖⽤户的需求⾜够了

这个误区下会认为,端到端测试是站在⽤户视⻆做测试,把⽤户要的功能点都覆盖到就⾜够了。这种误区导致的结果就是冰激凌筒反⾯模式。虽然软件交付的最终功能是给客户使⽤的,但构成软件的代码本身是给⼈(研发)读的、需要⼈去维护。外部⽤户是⼈,内部⽤户也是⼈。

常⻅误区⼆:All-in端到端测试,节省了80%的测试代码量,赢麻了

从短期来看,不写单测可以节省80%的测试代码量和⾄少50%的研发时间。但只要项⽬复杂起来,时间线拉⻓,过去⽋的历史债务(technical debt)早晚要加倍奉还。等到真正需要还债的时候再去补,可能为时已晚。

常⻅误区三:写单测的⼈都弱爆了,我⻓这么⼤还没写出过bug

这篇⽂章可能不适合你。不过软件开发是个团队项⽬,你写的代码最终也会落到别⼈⼿⾥去升级维护,没有测试覆盖的代码是没⼈敢碰的。

总结

结尾处再快速总结⼀下。本⽂从测试体系的历史⼊⼿,讲述了从⼿动测试 -> 靠别⼈⾃动化测试 -> 靠⾃⼰⾃动化测试的历史演化进程,也尝试着从这个视⻆解释为什么⼤家过去不重视单元测试。之后我们分别讲述了什么是单元测试,业界的⾦字塔测试佳实践,并且深⼊讲解了单元测试的种种好处。后我们列举了常⻅的反⾯模式和误区,帮助⼤家快速识别规避常⻅的错误。

如果把测试体系的演进类⽐为⼈类的进化,那么我认为⽆单测覆盖和有充分单测覆盖的软件就好⽐爬⾏的古猿和直⽴⾏⾛的现代⼈类。由衷希望⼤家能够重视单元测试、写好单元测试,让我们的软件尽快从爬⾏进化成奔跑,迸发出源源不断的⽣命⼒、创造出更多价值!

参考资料:

[1]https://abseil.io/resources/swe-book/html/ch11.html https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html

[2]https://arstechnica.com/information-technology/2014/08/how-microsoft-dragged-itsdevelopment-practices-into-the-21st-century/4/

[3]https://medium.com/nerd-for-tech/the-paradigm-shifts-going-from-1-1-to-10-1-to-100-1-dev-testratio-44183a734d77

[4]https://blog.testproject.io/2018/11/06/the-software-engineer-in-test/

相关文章
|
6月前
|
人工智能 机器人 Java
AI场景面试题
基于150场面试统计,AI相关问题占比22%(32场)。常见问题涵盖AI模块设计、模型训练与部署(如Ollama、MaxKB)、RAG技术、千帆大模型接入、Spring AI框架、AIGC应用及模型微调等,聚焦实际项目中AI落地的技术细节与优化策略。
|
6月前
|
存储 消息中间件 开发框架
应用架构图
技术架构是将业务需求转化为技术实现的关键过程,涵盖分层设计、技术选型与系统集成。本文详解单体与分布式架构,包括展现层、业务层、数据层及基础层的职责,并阐述应用间调用关系、外部系统集成与边界划分,助力构建清晰的技术体系。
|
6月前
|
缓存 NoSQL 调度
项目《神领物流》
本项目为基于微服务架构的智能物流系统,涵盖用户端、快递员端、司机端及管理端。采用Spring Cloud、RabbitMQ、Redis、MongoDB、Neo4j等技术,实现智能调度、路线规划、运费计算、权限管理、多级缓存与分布式事务等功能,提升运输效率,降低运营成本。
|
6月前
|
人工智能 Java 网络安全
Spring AI Alibaba:本地运行(☆)
简介:本任务要求使用SSH方式拉取私有Git仓库代码,基于SpringCloud、MySQL、Maven技术栈,完成聊天机器人、智能体、工作流三大功能模块的本地运行。需录制8分钟以上视频,结构化输出项目理解,包括技术栈、核心功能、数据库关系及未解困惑,帮助新人快速融入开发环境。(239字符)
 Spring AI Alibaba:本地运行(☆)
|
6月前
|
存储 Java 数据库
项目《四方保险》
本系列内容围绕《四方保险》系统展开,涵盖系统架构、数据库设计、保险产品组成与分类、微服务划分、文件上传与垃圾处理、性能优化、AOP应用、保费计算逻辑、支付流程、埋点设计及短信平台实现等核心话题,全面梳理保险系统开发中的关键技术与业务实践。
|
6月前
|
NoSQL 算法 Java
项目《天机学堂》
天机学堂是一个非学历职业技能在线培训平台,核心业务为售卖课程并提供学习辅助与交互功能。技术栈涵盖SpringBoot、Redis、RabbitMQ等。本人负责需求分析、数据库设计及通用工具封装,如基于Redisson实现分布式锁组件,支持注解式加锁、锁类型切换与限流;并参与开发高性能视频进度记录系统,通过缓存+异步持久化方案实现秒级精度回放,有效降低数据库压力。
|
6月前
|
存储 人工智能 Cloud Native
云原生数据仓库 AnalyticDB Supabase 使用全攻略
云原生数据仓库 AnalyticDB PostgreSQL 版 Supabase 是基于开源 Supabase 深度增强的全托管平台,兼容 Supabase 生态,提供数据库、用户认证、边缘函数等核心能力,并集成通义千问等 AI 模型,支持 Vibe Coding 与智能应用快速开发。原生支持微信、支付宝 OAuth,具备企业级安全与全链路可观测性,助力开发者高效构建 AI 原生应用。
|
6月前
|
消息中间件 Java Kafka
在 OpenAI 打造流处理平台:超大规模实时计算的实践与思考
本文介绍OpenAI构建流处理平台的实践与挑战。面对Kafka高可用、Python生态兼容、云环境限制等问题,团队基于PyFlink打造跨区域流处理架构,集成Kafka HA组、自研代理与控制平面,支撑实时Embedding生成、特征计算等场景,并推动开源协作与平台自动化演进。
431 1
在 OpenAI 打造流处理平台:超大规模实时计算的实践与思考
|
6月前
Dataphin功能Tips系列(83)大模型加持码表构建,从杂乱数据中智能提取标准代码
某电商多平台订单数据因标准不一难以对齐。通过Dataphin 5.4的X-数据标准Agent,利用大模型语义分析与数据探查,智能逆向抽取码表,快速构建统一编码体系,显著提升数据治理效率与标准化水平。
166 0
|
6月前
|
传感器 算法 机器人
减速机行业场景化技术方案与数据化选型指南
本文系统解析减速机在新能源、机器人、智能物流等领域的场景化应用,结合15年工程经验与12个典型案例,构建涵盖选型校核、结构优化、维护策略的全流程技术方案,配套数据化计算工具与故障诊断算法,助力实现精准匹配、可靠运行与降本增效。
减速机行业场景化技术方案与数据化选型指南

热门文章

最新文章