单元测试最佳实践

一、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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容

  • 前言 以前单元测试在JavaScript项目中配置其实还是挺繁琐的,依赖各种库mocha,chai,sion或者第...
    yellow超阅读 2,950评论 0 5
  • 前言 在工作中或者在面试中,我经常碰到的开发人员就是对单元测试不重视,这一类基本上都表现出了一种“无知的自信”,总...
    敏捷的水阅读 5,618评论 6 26
  • 原文地址:Unit testing best practicesPS:本文未翻译原文的全部内容,以下为译文。 编写...
    雪飞鸿阅读 1,201评论 2 6
  • 目的 充分的单元测试就是提高代码质量最有效的手段之一,而单元测试严重依赖代码的可测试性,本文主要通过一个简单的DE...
    核子飞弹阅读 1,488评论 1 13
  • 推荐指数: 6.0 书籍主旨关键词:特权、焦点、注意力、语言联想、情景联想 观点: 1.统计学现在叫数据分析,社会...
    Jenaral阅读 5,706评论 0 5