一、What?什么是单元测试
没有严格定义被测单元的大小
通常在类级别或一小组相关类的周围编写
测试重点是被测单元
开发阶段的自动化测试
所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次可以保证能够代码覆盖率达到100%的测试
二、When?什么时候做单元测试
开发阶段
不能一刀切,不能只盯着单测阶段的耗时
增量还是存量
单测case针对增量代码
当存量代码出现大规模重构,后者质量暴露出极大风险时,都是推动补全单测的好时机
三、Which?单元测试范围
既可以针对一个函数写case,也可以按照函数的调用关系串起来写case。
推荐优先测试以下模块
核心代码
复用性代码,例如Utils类
逻辑复杂代码,例如Manager 层,可重用度高的 Service
语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%。 --- 《阿里巴巴Java开发手册》
单测覆盖度分级参考
Level1:正常流程可用,即一个函数在输入正确的参数时,会有正确的输出
Level2:异常流程可抛出逻辑异常,即输入参数有误时,不能抛出系统异常,而是用自己定义的逻辑异常通知上层调用代码其错误之处
Level3:极端情况和边界数据可用,对输入参数的边界情况也要单独测试,确保输出是正确有效的
Level4:所有分支、循环的逻辑走通,不能有任何流程是测试不到的
Level5:输出数据的所有字段验证,对有复杂数据结构的输出,确保每个字段都是正确的
四、How?如何写单元测试
准备工具
Junit -- Java主流单测框架,也有用TestNG
Mockito -- Java的Mock框架
PowerMock -- Mockito增强,还覆盖了private/static/final/constructor method
Jacoco -- 代码覆盖率工具,支持多种尺度的覆盖率计数器,例如类、方法、代码、行、分支、甚至指令
Sonar -- 代码质量管理平台,支持多语言代码质量管理与检测,支持可视化分析jacoco报告
用例设计法
思考下如何编写用例??
需求覆盖(测试角度):
指的是测试人员对需求的了解程度,根据需求的可测试性来拆分成各个子需求点,来编写相应的测试用例,最终建立一个需求和用例的映射关系,以用例的测试结果来验证需求的实现,可以理解为黑盒覆盖。
逻辑(代码)覆盖(开发角度):
为了更加全面的覆盖,我们可能还需要理解被测程序的逻辑,需要考虑到每个函数的输入与输出,逻辑分支代码的执行情况,这个时候我们的测试执行情况就以代码覆盖率来衡量,可以理解为白盒覆盖。
基于意图:
思考函数最终想做什么,把被测函数当做黑盒,考虑其输出输出,而不要关注其中间是怎样实现的。
基于实现:
输入输出我也考虑,中间怎么实现的我也考虑。mock就是一个好例子
黑盒法:
等价类:正确的,错误的(合法的,非法的)
边界法:[1,10] ==> 0,1,2,9,10,11(是等价类的有效补充)
白盒法:
逻辑覆盖(语句、分支、条件、条件组合等)
路径(全路径、最小线性无关路径)
循环:结合5种场景(跳过循环、循环一次,循环最大次,循环m次命中、循环m次未命中)
注意单测“过度设计”,一旦代码重构,会出现大批单测失败。
BCDE原则 --- 《阿里巴巴Java开发手册》
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等
C:Correct,正确的输入,并得到预期的结果
D:Design,与设计文档相结合,来编写单元测试
E:Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期的结果
Given/When/Then
Given: 准备数据,给定上下文
When: 执行操作
Then: 验证
public class SomeClassTest {
@Before
public void setup() {}
@Test
public void shouldReturnItemNameInUpperCase() {
// Given
Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
when(itemRepository.findById("it1")).thenReturn(mockedItem);
// When
String result = itemService.getItemNameUpperCase("it1");
// Then
assertThat(result, is("ITEM 1"));
}
@After
public void teardown() {}
}
单测案例
Condition
马赛克马赛克马赛克马赛克马赛克马赛克
Void
马赛克马赛克马赛克马赛克马赛克马赛克
Private(不推荐)
马赛克马赛克马赛克马赛克马赛克马赛克
Static
马赛克马赛克马赛克马赛克马赛克马赛克
Exception
马赛克马赛克马赛克马赛克马赛克马赛克
五、最佳实践
良好单测的特征
FIRST
FAST快速
对于大型成熟项目可能会有数千个测试用例。每个测试用例应尽可能快的运行,最好在毫秒级别。
Independent隔离
单元测试是独立的,可以单独运行而不依赖外部元素,如文件系统或数据库。
Repeatable可重复
在不改变输入的情况下,单元测试的输出结果应保持不变。
Self-validating自检查
单元测试应自动检测测试是否通过而无需人工检查。
Timely耗时少
如果测试代码所花费的时间远超编写代码的时间,应当考虑重构代码以便于更好测试。
AIR
Automatic(自动化)
单元测试应该是全自动执行的,并且非交互式的。测试框架通常是定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
Independent(独立性)
保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
反例:com.huami.service.id.services.AppServiceIT
Repeatable(可重复)
保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
最佳实践
命名规范
完整的用例和良好的命名可以做到自描述
https://vitalflux.com/7-popular-unit-test-naming-conventions/
https://osherove.com/blog/2005/4/3/naming-standards-for-unit-tests.html
正例:
test_orders_should_be_created();
testOrdersWithNoProductsShouldFail();
反例:
com.huami.service.admin.task.handler.UserDataExportHandlerTest
良好的可读性
用例需要符合3A原则。“Arrange、Act、Assert”是单元测试时的常见模式。
安排对象,根据需要对其进行创建和设置。
作用于对象。
断言某些项按预期进行。
控制代码复杂度
过于复杂的代码可测性不好
避免过多的条件语句。多层条件语句建议使用卫语句(guard clauses)、策略模式、状态模式等方式重构
反例:com.huami.service.id.services.changeUserState
测试驱动的开发与较低圈复杂度值之间存在着紧密联系
一个好的测试用例设计经验是:创建数量与被测代码圈复杂度值相等的测试用例,以此提升测试用例对代码的分支覆盖率。
映射到目前云端情况,单测倒逼重构
圈复杂度
麦凯布最早提出一种称为“基础路径测试”(Basis Path Testing)的软件测试方式,测试程序中的每一线性独立路径,所需的测试用例个数即为程序的圈复杂度
圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。
程序的可能错误和高的圈复杂度有着很大关系
无法复制加载中的内容
认知复杂度
Sonarqube工具设计的算法,作为圈复杂度的补充。它将一段代码被阅读和理解时的复杂程度,估算成一个具体数字。一个方法的认知复杂度基于以下三条简单规则
代码中用到一些语法糖,把多句话缩为一句:代码不会变得更复杂;
出现"break"中止了线性的代码阅读理解,如出现循环、条件、try-catch、switch-case、一串的and or操作符、递归,以及jump to label:代码因此更复杂;
多层嵌套结构:代码因此更复杂;
认知复杂度制定的主要目标,是为方法计算出一个得分,准确地反应出此方法的相对理解难度。
单元测试粒度尽可能细
单测粒度至多是类级别,一般是方法级别。
单测越大编写成本越高,并容易失败。
避免在同一个测试用例中使用多个断言
反例:com.huami.service.login.controllers.RegistrationControllerTest#checkRegistrationAvailable
使用帮助方法来构建和销毁测试依赖项
如果你的多个测试用例需要相似的对象或者状态,请使用帮助方法而不是Setup特性来获取它们。
测试用例中不要包含逻辑判断
避免在测试用例中引入BUG,关注测试结果而不是实现细节
反例:异常捕捉、if、for等
通过测试公共方法来验证私有方法
跳过private函数不好,提升访问权限算bad smell
不去直接测试private函数,好的private函数都应该是很小很简单的,测试那调用了private函数的public和protected方法即可。
或者,也许这个private函数其实应该被声明称protected。
某一个private函数很复杂,很需要测试。那么,根据Single Responsibility原则,这个private函数就应该被放到一个单独的class里面
TDD不存在这个问题,所有代码是测试驱动生长出来的
测试异常
编写用例需要充分考虑异常场景
Junit支持 expected、assertThrows 断言工具、使用 Rule等方式测试异常,不要使用try...catch...
不要滥用Mock
Mock在依赖隔离的同时,也使得测试场景逐渐偏离真实性,增加了测试风险。
如果一个对象具有以下特征,比较适合使用mock对象:
该对象提供非确定的结果(比如当前的时间或者当前的温度)
对象的某些状态难以创建或者重现(比如网络错误或者文件读写错误)
对象方法上的执行太慢(比如在测试开始之前初始化数据库)
该对象还不存在或者其行为可能发生变化(比如测试驱动开发中驱动创建新的类)
该对象必须包含一些专门为测试准备的数据或者方法
不必苛求100%覆盖率
有一些代码测试覆盖率很难提升,追求 100% 的代码覆盖率性价比非常低。
根据 2-8 原则,80% 的代码都是很好测试,且性价比高的,优先选择为他们编写测试。
比代码行覆盖率更重要的是,人们对于未覆盖代码(和场景)的判断,以及对于风险是否可接受。
增量代码覆盖率比全量代码覆盖率更实用
六、最后
测试左移是改革,不仅工作方式的改革,更是思想上的改革。
参考:
https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices