点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长

第一时间与你相约

每日英文

A life without if, but there are many but; Life can not regret it, but it can turn.

人生没有如果,但是有很多但是;人生不能后悔,但是可以拐弯。

每日掏心话

给生活一个微笑,给自己一个微笑,路还是要继续走。春有春的温暖,夏有夏的火热,秋有秋的收获,冬有冬的寒冷,学会享受生活!

来自:csonezp | 责编:乐乐

链接:cnblogs.com/csonezp/p/11757967.html

单元测试的优缺点(单元测试实战-四种覆盖详解)(1)

程序员小乐(ID:study_tech)第 674 次推文 图片来自网络

往日回顾:赛道爆发,玩家崛起,边缘计算CDN革了谁的命?

00 前言

单元测试,就是对某一段细粒度的Java代码的逻辑测试。代码块一般指一个Java 方法本身,所有外部依赖都需要mock掉,仅关注代码逻辑本身。

需要注意,单测的一个大前提就是需要清楚的知道自己要测试的程序块所预期的输入输出,然后根据这个预期和程序逻辑来书写case。

(这里需要注意的就是单测的预期结果 一定要针对需求/设计逻辑去写,而不是针对实现去写,否则单测将毫无意义,照着错误的实现设计出的case也很可能是错的)

01 覆盖类型

1、行覆盖 Statement Coverage

行覆盖(又叫语句覆盖)就是通过设计一定量的测试用例,保证被测试的方法每一行代码都会被执行一遍。

路径覆盖是最弱的覆盖方式。

实例:

本例仅需要一个case,即可实现行覆盖。test case 如下:

a

b

x

预期结果

TC1

2

0

3

6

这个用例就可以保证所有的行都被执行。

但是仅仅有这一个用例的话,对这个方法的测试就是非常脆弱的。

举个栗子,某RD接到了这个需求,理清了逻辑,写好单测之后开始写代码(或者写好代码之后开始写单测)。但是由于手抖,将第三行的 && 写成了 ||:

然后跑一下单测,发现很顺滑,一下就过了。

随后该RD很高兴的将代码发布到了线上,结果就发生了严重的生产故障,于是该RD就被开除了。

行覆盖是一个最基础的覆盖方式,但是也是最薄弱的,如果完全依赖行覆盖,那不小心就会被开除。

2、判定覆盖 / 分支覆盖 (Decision Coverage/Branch Coverage)

判定覆盖的含义就是代码里每一个判定都要走一次true,一次false。依然用上面的代码,想要实现判定覆盖,需要以下case

a

b

x

预期结果

TC2

2

0

1

4

TC3

3

1

1

1

这两个用例可以保证判定 A:(a > 1 || b == 0) 和判定B:(a == 2 || x > 1) 分别都取一次true 和false:

tc2 时, A,B均为true;tc3时,A,B均为false。

可以看出分支覆盖依然有明显缺陷,并没有覆盖到 A: true B: false 和 A:false B:true的情况。

3、条件覆盖 Condition Coverage

条件覆盖和判定覆盖类似,不过判定覆盖着眼于整个判定语句,而条件覆盖则着眼于某个判断条件。

条件覆盖需要保证每个判断条件的true false都要覆盖到,而不是整个判定语句。

例如,判定A (a > 1 || b == 0) ,只需要整个判定表达式分别取一次真假即可满足判定覆盖,而要满足条件覆盖,则需要判断条件 (a>1) 和 (b==0) 分别都取一次true false才算满足。

依然采用同样的代码,要想实现条件覆盖,则需要:

a

b

x

预期结果

TC4

2

0

3

6

TC5

0

1

0

0

这两个用例可以保证 (a > 1) (b==0) (a == 2) (x > 1) 四个条件都分别取true false。

很明显可以发现,这玩意儿依然是不全面的,这个例子里条件覆盖和判定覆盖存在同样的问题,覆盖的不够全面。

4、路径覆盖 Path Coverage

路径覆盖这个顾名思义就是覆盖所有可能执行的路径。

为了方便理解,这里先把流程图画出来。

单元测试的优缺点(单元测试实战-四种覆盖详解)(2)

  • @Test public void testFun3PathCoverage(){ Integer res = demoService.fun3(0,1,0); Assert.assertEquals(0,res.intValue());

  • res = demoService.fun3(3,0,-3); Assert.assertEquals(0,res.intValue());

  • res = demoService.fun3(2,1,3); Assert.assertEquals(4,res.intValue());

  • res = demoService.fun3(2,0,3); Assert.assertEquals(6,res.intValue());

  • }

  • 02 总结

    这是最常见的几种覆盖类型,行覆盖、判定覆盖、条件覆盖优缺点一致,都是较为简单,方便构造用例,并且能对程序质量有一定保证作用。

    路径覆盖最完善,但是在一些复杂的场景里,会带来测试代码指数级增长的副作用,这个对于绝大多数人都是无法接受的。

    一般情况下,行覆盖和判定覆盖同时做到已经是比较良好的单测代码了,jacoco的覆盖率统计也都是基于判定覆盖的。

    03 实战部分(Mock)

    mock是什么

    事实上,理论和实际总是有差距的,单测也同样。

    虽然单测的指导性理论知识非常完备,但是实际工作中往往遇到各种各样的障碍。最常见的,莫过于各种外部依赖了,这些外部依赖总是会阻碍我们书写自己的单测代码。这时候,我们就要用到mock了。

    Mock这个单词的意思就是假的,模拟的。mock就是用一些特殊的代码,模拟一下外部依赖,将我们的测试代码与外部依赖解耦。

    最直接的mock,就是重新写一个外部依赖的mock类,在里面返回mock的数据。测试的时候,将外部依赖手动替换成mock类。但是这样mock需要频繁修改代码,基本没什么实际价值。

    为了解决mock外部依赖的需求,业界也出现了各种各样的mock框架。常见的有mockito、PowerMockito、Spock。

    Spock:比较新的测试框架,基于groovy。优点:语法优雅清晰,缺点:groovy需要学习,没有经历时间的考验。

    Mockito:最常用,最可靠的测试框架,优点就是没有什么太大的缺点。

    PowerMock:为了解决Mockito等基于cglib的框架无法mock 私有、静态方法而产生的框架。

    这里都采用Mockito来作为使用的测试框架。

    04 简单Mock实战

    整体代码详见:https://github.com/csonezp/mockdemo

    这里就写一些关键代码。

    service是最需要关注的地方,重要的逻辑一般都在这里;同时又有诸多的外部依赖,用这一层做实际mock的实例是最合适的。

    这个service很简单,这里针对里面的addUser方法写一些对应的单测:

    集成测试

    上面所说的都是单元测试,但是实际开发中,我们往往不光需要单元测试(甚至不需要单元测试。。。。),还需要有集成测试,来测试我们程序的整体运行情况。

    集成测试并不是联调,集成测试用依然可以mock第三方依赖。

    在我们的工程里,一般只要实际启动整个spring容器的测试代码,都是集成测试。

    Controller层是较为适合集成测试的地方,这里用Controller层来做集成测试的示例。

    下面写一个这次集成测试用的spring测试文件,位置如下:

    单元测试的优缺点(单元测试实战-四种覆盖详解)(3)

    测试代码:

    配置文件如下:

    这个配置文件的主要作用就是将程序连接的DB换成一个内存数据库h2,这样就不用在测试时受DB的掣肘了。

    @ActiveProfiles("it") 这个注解就是制定本测试代码加载的配置文件,”it“指 文件名 application-XX.properties 中间的xx,spring会自动根据名称去加载对应的配置文件。

    init() 方法就是在内存数据库中构造自己需要的数据,这是集成测试最常见的步骤。

    后面的测试代码就不需要解释太多了。

    欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

    欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

    猜你还想看

    阿里、腾讯、百度、华为、京东最新面试题汇集

    Spring、Spring MVC、MyBatis 整合文件配置详解,看了都说好!

    初级开发者应该从Spring源码中学什么?

    Java 操作Word表格——创建嵌套表格、添加/复制表格行或列、设置表格是否禁止跨页断行

    关注微信公众号「程序员小乐」,收看更多精彩内容

    嘿,你在看吗?

    ,