为什么需要写Java单元测试

前言

  为了保证代码质量,在写完代码后,写单测是很有必要的。当然,在大部分情况下,我们可能不会写单测,而是直接把应用部署起来,直接自测,然后再联调。估计很大一部分人,都是用这种方式开发。当然,我之前也是按这个方式来开发,单测覆盖率纯粹是为了满足公司的指标要求,大部分流于形式。

  一、为什么写单元测试

  说到单元测试,就不得不提起另一个词,TDD(Test-Driven Development)测试驱动开发:在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码

  测试驱动开发虽然饱受争议,不过有这种方法论的推出并有不少的同行在践行,起码能够说明测试的重要性。

  1、当我们想测试部分代码逻辑是否正常的时候,我们可能会直接psvm来构造数据进而调试。那如果有一种东西能把我们psvm统一放到某个地方呢?

  2、当我们在一个系统里边修改了很多代码时,又不确定改动是否影响在核心逻辑时。那如果有一种东西能在编译的时候,顺便自动跑一遍逻辑做回归呢?无论是重构还是正式提测前,都提高了自己写代码的信心。

  3、当我们很容易一不小心时就把代码写成一坨屎,那如果有一种东西能让我们在编码的时候就注重自己的代码设计呢?

  4、当我们这个季度什么都没干,但是系统没发生过故障,那如果有一种东西能让我们在KPI上添上浓墨的一笔呢?

  5、....欢迎补充

  没错,这东西就是单元测试。

  写单测好处

  慢慢的,我感受到写单测带来的几个好处:

  1、提升效率

  启动一个应用,几分钟,找bug,修bug,再重启应用,这个过程不断的重复,应用重启太浪费时间。

  而单测不需要重启整个应用,只对几个service做测试,效率高很多。

  2、场景覆盖全

  单测可以对代码运行中的各种情况进行模拟,并对最终的返回结果断言,这是自己自测很难模拟的。而且,这些单测,是沉淀的资产。下次修改代码,可以重跑以前单测,发现问题,避免踩坑。

  单测怎么写

  我们很容易对单测产生误解,所以这里我先把2个概念说明一下:

  1、集成测试

  测试过程中,会启动整个Spring容器,调用DB 或者 依赖的外部接口等。只不过访问的环境是测试环境。这个过程最大程度还原生产环境过程,但是耗时长。

  2、单元测试

  不启动整个应用,只对单个接口/类进行测试。不调用DB 、外部接口,依赖的服务都Mock掉,只测试代码逻辑。这个过程,测试用例耗时短。

  我们说的单测,是指第2种。

  单测过程分2步,第一步:Mock外部依赖,第二步:断言

  Mock框架

  1、Mockito单元测试

 <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
       <version>3.3.3</version>
       <scope>test</scope>
   </dependency>

 

2、Mockito 中文文档地址

  Mockito库能够Mock对象、验证结果以及打桩(stubbing)。

  GitHub - hehonghui/mockito-doc-zh: Mockito框架中文文档

  二、强制要求

  1.好的单元测试必须遵守AIR原则。

  ·A:Automatic(自动化)

  · I:Independent(独立性)

  · R:Repeatable(可重复)

  单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。

  2.单元测试应该是全自动执行的,并且非交互式的。

  测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试,不是一个好的单元测试。

  单元测试中不准使用System.out来进行人肉认证,必须使用assert来验证。

  3.保持单元测试的独立性。

  为了保证单元测试稳定可靠且便于维护,单元测试用例之间绝不能互相调用,也不能依赖执行的先后次序。

  4.单元测试是可以重复执行的,不能受到外界环境的影响。

  单元测试通常会被放到持续集成中,每次有代码check in时单元测试都会被执行。

  如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续继承机制的不可用。

  正例:为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI框架注入一个本地(内存)实现或者 Mock 实现。

  被测系统

  被测系统(System under test, SUT)表示正在被测试的系统,目的是测试系统能否正确操作。

  根据测试类型的不同, SUT 指代的内容也不同, 例如 SUT 可以是一个类甚至是一整个系统。

  5.对于单元测试,要保证测试粒度足够小。

  有助于精确定位问题,单测粒度至多是类级别,一般是方法级别。

  6.核心业务、核心应用、核心模块的增量代码,确保单元测试通过。

  说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。

  7.单元测试代码目录

  必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。

  说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。

  三、推荐要求

  1.单元测试的基本目标

  语句覆盖率达到70%,核心模块的语句覆盖率和分支覆盖率都要达到100%。

  说明:在DAO层,Manager层和可重用度高的Service中都应该进行单元测试。

  分支覆盖率 :5个分支,那么对应的应该有10条语句(一个分支有两条语句,ture和false),如果你执行了其中的5条,那么覆盖率就是50%。

  2.编写单元测试代码时遵守BCDE原则,以保证被测试模块的交付质量

  B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。

  C:Correct,正确的输入,并得到预期的结果。

  D:Design,与设计文档相结合,来编写单元测试。

  E:Error,强制错误信息输入,并得到预期的结果。

  3.对于数据库的相关查询、更新和删除等操作

  不能假设数据库里的数据是存在的,或者直接操作数据库将数据插进去,请使用程序插入或者导入数据的方式来准备数据。

  4.和数据库相关的单元测试

  可以设定自动回滚机制,不给数据库造成脏数据,或者对单元测试产生的数据有明确的前后缀标识。

  正例:在企业智能事业部的内部单元测试中,使用 ENTERPRISE_INTELLIGENCE UNIT_TEST 的前缀来标识单元测试相关代码。

  5.对于不可测的代码

  在适当的时机做必要的重构,使代码变得可测,避免为了达到测试要求而书写不规范测试代码。

  6.在设计评审阶段

  开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例。

  7.单元测试作为一种质量保障手段

  在项目提测前完成单元测试,不建议项目发布后补充单元测试用例。

  四、参考要求

  1.为了更方便地进行单元测试,业务代码应该避免一下情况

  ·构造方法中做的事情过多

  · 存在过多的全局变量和静态方法

  · 存在过多的外部依赖

  · 存在过多的条件语句

  说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。

  如果条件语句极其复杂,就应该将条件语句拆解开,然后逐个检查,并在条件为真时立刻从函数中返回,这样的单独检查通常被称之为“卫语句”(guard clauses)

  摘自《重构---改善既有代码的设计》

  卫语句的效果就是将原来需要仔细阅读代码、细心整理逻辑的条件判断整理成一眼能看透的逻辑关系,效果就像以下:

 if(obj != null){
     doSomething();
   }

 转换成卫语句以后的代码如下:

 if(obj == null){
     return;
   }
    doSomething();

2.不要对单元测试存在如下误解

  ·那是测试同学干的事情。单元测试也是和开发同学强相关的。

  · 单元测试代码是多余的。系统的整体功能和各单元部件的测试真长与否是强相关的。

  · 单元测试代码不需要维护。一年半载后单元测试几乎都会处于废弃朱状态。

  · 单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。

  五、单元测试与集成测试的区别

  在实际工作中,不少同学用集成测试代替了单元测试,或者认为集成测试就是单元测试。

  单元测试与集成测试的区别:

  1.测试对象不同

  单元测试对象是实现了具体功能的程序单元,集成测试对象是概要设计规划中的模块及模块间的组合。

  2.测试方法不同

  单元测试中的主要方法是基于代码的白盒测试,集成测试中主要使用基于功能的黑盒测试

  3.测试时间不同

  集成测试要晚于单元测试。

  4.测试内容不同

  单元测试主要是模块内程序的逻辑、功能、参数传递、变量引用、出错处理及需求和设计中具体要求方面的测试;而集成测试主要验证各个接口、接口之间的数据传递关系,及模块组合后能否达到预期效果。

  六、为什么要使用Mock做测试

  比如你现在想要测试一个方法是否是正常的,但是这个方法中有很多调用数据库的代码,那么我们就可以在每个调用数据库的地方打桩,模拟一下访问完数据库之后的返回值,这样我们就可以在测试的时候避免访问数据库了,可以非常高效地完成我们的单元测试,已达到验证我们写的方法到底对不对的目的。

  1.Mock可以用来解除外部服务依赖,从而保证了测试用例的独立性。

  现在的互联网软件系统,通常采用了分布式部署的微服务,为了单元测试某一服务而准备其它服务,存在极大的依耐性和不可行性。

  2.Mock可以减少全链路测试数据准备,从而提高了编写测试用例的速度。

  传统的集成测试,需要准备全链路的测试数据,可能某些环节并不是你所熟悉的。最后,耗费了大量的时间和经历,并不一定得到你想要的结果。现在的单元测试,只需要模拟上游的输入数据,并验证给下游的输出数据,编写测试用例并进行测试的速度可以提高很多倍。

  3.Mock可以模拟一些非正常的流程,从而保证了测试用例的代码覆盖率。

  根据单元测试的BCDE原则,需要进行边界值测试(Border)和强制错误信息输入(Error),这样有助于覆盖整个代码逻辑。在实际系统中,很难去构造这些边界值,也能难去触发这些错误信息。而Mock从根本上解决了这个问题:想要什么样的边界值,只需要进行Mock;想要什么样的错误信息,也只需要进行Mock。

  4.Mock可以不用加载项目环境配置,从而保证了测试用例的执行速度。

  在进行集成测试时,我们需要加载项目的所有环境配置,启动项目依赖的所有服务接口。往往执行一个测试用例,需要几分钟乃至几十分钟。采用Mock实现的测试用例,不用加载项目环境配置,也不依赖其它服务接口,执行速度往往在几秒之内,大大地提高了单元测试的执行速度。

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

 

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值