【编码心得】单元测试的那些事
文章目录
- 单元测试定义?
- 为什么需要单元测试?
- 为重构保驾护航
- 提高代码质量
- 减少 bug
- 快速定位 bug
- 持续集成依赖单元测试
- 团队编码规范要求
- 大牛都写单元测试
- 保住面子
- TDD 测试驱动开发
- 何谓 TDD?
- TDD的基本流程
- TDD 优缺点分析
- 单测框架如何选择?
- 一、选择标准
- 二、推荐的Java单测框架
- 三、选择建议
- AI时代下的单元测试
- 总结
单元测试定义?
单元测试是指对软件中的最小可测试单元(如函数、方法、类等)进行隔离测试的过程。这些单元在测试过程中被单独运行,以确保它们的行为符合预期。单元测试是软件开发中最低级别的测试活动,旨在发现模块内部存在的各种错误。
在做单元测试时,为了隔离外部依赖,确保这些依赖不影响验证逻辑,我们经常会用到 Fake、Stub 与 Mock 。
关于 Fake、Mock 与 Stub 这几个概念的详细解析:
1. Fakes(伪对象)
定义: Fakes是那些包含了生产环境下具体实现的简化版本的对象。它们不是完整的生产实现,但会采取一些捷径,拥有生产代码的简化版本。
特点: Fakes关注于核心逻辑,去除多余的内容。例如,在测试过程中,可能不需要真实的数据库操作,而Fakes则可以通过内存中的数据结构来模拟数据库操作,从而简化测试步骤。
Fakes可以用于原型设计或峰值模拟中,以快速实现系统原型并基于内存存储来运行整个系统。
另一个常见的使用场景是利用Fakes来保证在测试环境下支付、邮件发送等操作永远返回成功结果,以避免外部依赖对测试的影响。
2. Mocks(模拟对象)
定义: Mocks是一种特殊的测试双体,它们允许测试者定义对象的行为并验证这些行为是否被调用。Mocks可以模拟对象的方法调用、返回值以及异常等。
特点: Mocks能够使得测试失败,这意味着它们可以人为地制造一些异常或特定条件来验证程序对这些异常或条件的处理方式是否如预期。
Mocks更适合于测试那些具有复杂依赖或外部交互的功能,如发送邮件、访问数据库等。
通过Mocks,测试者可以确保在测试过程中不会受到外部系统或依赖的干扰,从而专注于测试目标本身。
3. Stubs(存根对象)
定义: Stubs是那些包含了预定义好的数据并且在测试时返回给调用者的对象。它们通常用于替代那些难以控制或需要复杂设置的依赖。
特点: Stubs关注于输入输出,即它们会伪造一个输入并返回一个预期的输出,以验证被测方法的正确性。
Stubs常用于查询(Query)方法的测试中,这些方法通常只返回数据而不改变系统状态。
通过Stubs,测试者可以简化测试用例的编写,无需担心外部依赖的复杂性和不确定性。
为什么需要单元测试?
为重构保驾护航
单元测试可以为重构提供信心,降低重构的成本。我们要像重视生产代码那样,重视单元测试。
每个开发者都会经历重构,重构后把代码改坏了的情况并不少见,很可能你只是修改了一个很简单的方法就导致系统出现了一个比较严重的错误。
提高代码质量
由于每个单元有独立的逻辑,做单元测试时需要隔离外部依赖,确保这些依赖不影响验证逻辑。因为要把各种依赖分离,单元测试会促进工程进行组件拆分,整理工程依赖关系,更大程度减少代码耦合。这样写出来的代码,更好维护,更好扩展,从而提高代码质量。
减少 bug
一个可单元测试的工程,会把业务、功能分割成规模更小、有独立的逻辑部件,称为单元。单元测试的目标,就是保证各个单元的逻辑正确性。单元测试保障工程各个“零件”按“规格”(需求)执行,从而保证整个“机器”(项目)运行正确,最大限度减少 bug。
快速定位 bug
如果程序有 bug,我们运行一次全部单元测试,找到不通过的测试,可以很快地定位对应的执行代码。修复代码后,运行对应的单元测试;如还不通过,继续修改,运行测试……直到测试通过。
持续集成依赖单元测试
持续集成需要依赖单元测试,当持续集成服务自动构建新代码之后,会自动运行单元测试来发现代码错误。
团队编码规范要求
有些经验丰富的领导,或多或少都会要求团队写单元测试。对于有一定工作经验的队友,这要求挺合理
大牛都写单元测试
大牛的自我修养,膜拜学习,坚持写高质量单测
保住面子
都是有些许年经验的老鸟,还天天被测试同学追 bug,好意思么?花多一点时间写单元测试,确保没低级 bug,也怕别人不小心改了自己的代码,新版本上线提心吊胆……花点时间写单元测试,有事没事跑一下测试,确保原逻辑没问题,至少能睡安稳一点。
TDD 测试驱动开发
何谓 TDD?
TDD(Test Driven Development,测试驱动开发)是敏捷开发中的一项核心实践和技术,也是一种设计方法论。其核心思想是在编写实际代码之前先编写测试代码,然后根据测试代码来驱动实际代码的编写。以下是对TDD的详细解释:
TDD的基本流程
TDD的基本流程通常包括以下几个步骤:
- 编写测试案例:首先,根据需求编写测试案例,测试案例通常包括输入数据、预期输出以及测试方法。
- 运行测试案例:运行编写的测试案例,此时测试案例会失败,因为还没有编写代码来满足测试需求。
- 编写最小量代码:根据测试案例编写最小量代码以使测试通过。
- 再次运行测试案例:确保新编写的代码能够通过所有的测试案例。
- 重构代码:对代码进行重构,保持代码质量,消除冗余,提高可读性。
这个过程是一个迭代的过程,不断重复“编写测试案例—编写代码—运行测试—重构代码”的步骤,直到完成所有功能的开发。
TDD 优缺点分析
测试驱动开发有好处也有坏处。因为每个测试用例都是根据需求来的,或者说把一个大需求分解成若干小需求编写测试用例,所以测试用例写出来后,开发者写的执行代码,必须满足测试用例。如果测试不通过,则修改执行代码,直到测试用例通过。
优点:
- 帮你整理需求,梳理思路;
- 帮你设计出更合理的接口(空想的话很容易设计出屎);
- 减小代码出现 bug 的概率;
- 提高开发效率(前提是正确且熟练使用 TDD)。
缺点:
- 能用好 TDD 的人非常少,看似简单,实则门槛很高;
- 投入开发资源(时间和精力)通常会更多;
- 由于测试用例在未进行代码设计前写;很有可能限制开发者对代码整体设计;
- 可能引起开发人员不满情绪,我觉得这点很严重
单测框架如何选择?
以下是一些关键的选择标准和推荐的Java单测框架:
一、选择标准
兼容性:
确保所选框架与Java版本及项目依赖库兼容。
社区支持:
活跃的社区意味着有更多的文档、教程和问题解决方案。
易用性:
框架的学习曲线应平缓,易于上手,同时提供清晰的错误信息和调试支持。
扩展性:
支持自定义测试用例、测试套件和插件,以满足不同的测试需求。
集成能力:
能够与持续集成(CI)工具、IDE和其他开发工具无缝集成。
功能特性:
提供丰富的断言方法、测试运行器、数据驱动测试等特性。
二、推荐的Java单测框架
JUnit
简介: JUnit是Java编程语言中最常用的单元测试框架之一。它提供了一个简单的框架来编写和运行可重复的测试。
特点: 丰富的断言方法,如assertEquals、assertNotNull等。
支持测试套件和测试运行器。易于与IDE和构建工具集成。
适用场景: 适用于大多数Java项目的单元测试。
TestNG
简介:TestNG是JUnit的一个替代品,它提供了更丰富的测试用例定义和配置方式。
特点: 支持测试套件、数据驱动测试、依赖测试、并行测试等高级特性。使用XML文件进行配置,支持复杂的测试场景。提供多种测试运行器和扩展插件。
适用场景: 适用于需要复杂测试场景和高级特性的项目。
Mockito
简介: 虽然Mockito本身不是一个完整的单元测试框架,但它是一个流行的Mocking框架,经常与JUnit等框架结合使用。
特点: 允许开发者创建和配置Mock对象,以模拟外部依赖的行为。提供丰富的API来定义Mock对象的行为和验证它们的交互。易于与JUnit等框架集成。
适用场景: 适用于需要Mock外部依赖以进行隔离测试的场景。
三、选择建议
根据项目需求选择: 根据项目的具体需求和团队的实际情况选择合适的框架。例如,如果项目需要复杂的测试场景和高级特性,可以考虑使用TestNG;如果项目主要关注单元测试,并且需要Mock外部依赖,那么JUnit结合Mockito可能是一个不错的选择。
考虑社区支持: 选择拥有活跃社区和广泛用户基础的框架,以便在遇到问题时能够获得及时的帮助和支持。
评估学习成本: 考虑团队成员对所选框架的熟悉程度和学习成本,选择易于上手且能够快速提高测试效率的框架。
集成与扩展性: 确保所选框架能够与现有的开发工具和流程无缝集成,并支持未来的扩展需求。
AI时代下的单元测试
现在生成式AI的爆火,着实让UT的编写爽翻了,你敢想象:一个复杂service的单元测试的编写,只需要告诉ai:“帮我生成单元测试”,噼里啪啦生成了一堆单测,然后定睛一瞅,我去,还写得挺不赖,稍作修改就能使用! 科技发展提高生产力~~yyds
总结
单元测试确实会带给你相当多的好处,但不是立刻体验出来。写了可以买个放心,对代码的一种保障,有 bug 尽快测出来,以下是个人对单元测试一些建议:
- 遵循测试金字塔
测试金字塔是一个指导原则,它建议我们在项目中拥有不同级别的测试,但每种类型的测试数量应该有所不同。单元测试应该位于金字塔的底部,数量最多,因为它们运行速度快且容易编写。集成测试位于中间,而端到端测试(或系统测试)则位于顶部,数量较少。 - 保持测试的简洁性
每个单元测试都应该专注于测试一个小的、具体的功能点。避免在单个测试中测试多个逻辑路径或方法。如果测试变得复杂,考虑将其拆分成多个更小的测试。 - 编写可重复的测试
确保你的测试不依赖于外部状态或数据(如数据库、文件系统或网络状态)。使用mocking框架(如Mockito)来模拟依赖项,以便测试能够独立于这些外部因素运行。 - 使用断言来验证结果
断言是单元测试的核心。使用断言来明确验证你的代码是否按预期工作。JUnit 5提供了丰富的断言库,你可以使用这些断言来检查返回值、异常、集合内容等。 - 编写有意义的测试名称
测试方法的名称应该清晰地描述它们所测试的内容。避免使用模糊的名称,如test1()、testMethod()等。相反,使用像testAddTwoNumbers()、testUserCreationWithInvalidEmailThrowsException()这样的名称。 - 遵循测试驱动开发(TDD)
虽然TDD不是强制性的,但它是一种强大的实践,可以帮助你编写更清晰、更可维护的代码。TDD鼓励你先写测试,然后编写使测试通过的代码。这有助于你专注于当前正在实现的功能,并确保代码始终符合其预期用途。 - 关注代码覆盖率,但不要过度
代码覆盖率是衡量测试质量的一个重要指标,但它并不是唯一的指标。确保你的测试覆盖了关键路径和边界条件,但也要避免编写无用的测试,这些测试只是为了提高覆盖率而编写的。 - 自动化测试运行
将单元测试集成到你的构建流程中,以便在每次提交代码时自动运行测试。这有助于快速发现问题,并确保新代码不会破坏现有功能。 - 审查和重构测试代码
与应用程序代码一样,测试代码也需要进行审查和重构。随着应用程序的发展,测试代码可能会变得过时或冗余。定期审查和重构测试代码可以帮助你保持其质量和可维护性。 - 编写可读的测试代码
与应用程序代码一样,测试代码也应该易于阅读和理解。使用清晰的命名、注释和结构化代码来提高可读性。这将使其他开发者(或未来的你)更容易理解和维护测试代码。 - 越重要的代码,越要写单元测试;
代码做不到单元测试,多思考如何改进,而不是放弃;边写业务代码,边写单元测试,而不是完成整个新功能后再写;多思考如何改进、简化测试代码。测试代码需要随着生产代码的演进而重构或者修改,如果测试不能保持整洁,只会越来越难修改。
我是杰叔叔,一名沪漂的码农,下期再会!