导读
单元测试作为程序员的必修课,对代码的稳定性起着关键性的作用,但是你真的会写单元测试么?什么才算是真正的单元测试?这些疑问你都将在文章中得到解答。
在本文中,我们将主要基于Mockito框架来介绍如何编写单元测试,必要时使用PowerMock来对一些Mockito无法处理的方法进行操作,并且伴随有大量实例以助于理解。
1 什么是单元测试
什么是单元测试?我们先看看维基百科中对其的定义:
在计算机编程中,单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。
在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
根据以上定义,简单来说Java中单元测试就是对类中方法进行测试的工作。
2 单元测试的意义
2.1 为什么要进行单元测试
单元测试是软件测试的基础,不仅会直接影响到软件的后期测试,在很大程度上还会影响产品的最终质量。而且能写出高质量的单元测试代码,也是程序猿的基本修养之一。
● 提高代码质量,写过单元测试的代码,在提测、联调时bug数会明显减少,修改代码时也会对代码也会更有信心;
● 减少调试时间,如果认真的做好了单元测试,在系统集成联调时非常顺利,减少查找、调试bug时反复编译的时间成本;
● 在单元测试时某些问题就很容易发现,而在后期的测试中发现问题所花的成本将成倍数上升。
● 为代码重构保驾护航,可以更放心的去重构代码;
● 通过单元测试快速熟悉代码,比如代码做什么工作,有哪些特殊情况需要考虑,包含哪些业务。
2.2 什么时候写单元测试
编写单元测试的时机无非以下三种:
● 代码实现之前(TDD提倡);
● 与代码实现同步进行,开始写之前想好细节用例,然后一个个实现,代码一步步完善;
● 代码完成之后再写单元测试,效果不如前2种,而且单元测试的难度和工作量可能随着代码质量的变化成倍增加。
很多人觉得单元测试是在代码完成之后编写的,其实这种想法是错误的,根据TDD(Test-driven development)思想,倡导先写测试程序,然后再具体实现其功能。
但是很多人可能没有达到这种水平,此时退而求其次,边写代码边完成单元测试不妨为一种折中的选择。
2.3 哪些情况需要写单元测试
那么单元测试的粒度需要划分多细,哪些情况才需要写单元测试呢?以下四点可供参考:
● 涉及大量计算;
● 公共代码、工具类;
● 逻辑复杂、容易出错、不易理解;
● 核心业务代码。
3 单元测试的方法
在Sprig Boot环境下进行单元测试,首先需要在pom.xml中添加包依赖:
1<dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-test</artifactId>
4 <scope>test</scope>
5</dependency>
Spring Boot提供的spring-boot-starter-test启动器集成了常用的测试类库:
Spring Boot中单元测试类放在src/test/java目录下,通过IDEA可以自动创建测试类,快捷键为==⇧⌘T==(MAC)或者==Ctrl+Shift+T==(Window)。
还可以通过添加Coverage插件分析测试覆盖率。
3.1 Mock介绍
在写单元测试的过程中,一个很普遍的问题是,要测试的类可能会有很多的依赖,而这些依赖的类、对象又会有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
基于以上问题,我们引入了Mock方法,对被测试类所依赖的其他类和对象进行mock——构建它们的一个假对象,并定义这些假对象的行为,然后提供给被测试对象使用。
被测试对象像使用真的对象一样使用它们,这样我们就可以把测试的目标限定于被测试对象本身,从而实现依赖的隔离。
简单来说,mock对象就是在调试期间用来作为真实对象的替代品;mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法。
3.2 使用的框架
Mockito是一个针对Java的单元测试模拟框架,是为了简化单元测试过程中测试上下文(或者称之为测试驱动函数以及桩函数)的搭建而开发的工具。
3.3 测试流程
1@RunWith(MockitoJUnitRunner.class)
在被测试类之前加以上注解,表示使用指定运行器来运行测试类。MockitoJUnitRunner不需加载其他spring bean,也不需要启动spring的那一整套东西,启动速度非常快。
3.3.1 创建mock对象
对被测类中@Autowired的对象,用@Mock标注;
对被测类自己,用@InjectMocks标注。
被@Mock标注的对象会自动注入到被@InjectMocks标注的对象中。
@Spy可以实现部分mock,即不打桩时默认会执行真实的方法,如果打桩则返回桩实现。下面是官方给出的一个示例:
1 List list = new LinkedList();
2 List spy = spy(list);
3
4 //optionally, you can stub out some methods:
5 when(spy.size()).thenReturn(100);
6
7 //using the spy calls real methods
8 spy.add("one");
9 spy.add("two");
10
11 //prints "one" - 这个函数还是真实的
12 System.out.println(spy.get(0));
13
14 //100 is printed - size()函数被替换了
15 System.out.println(spy.size());
3.3.2 设置测试桩
也叫打桩,就是定制mock对象的具体行为,通过它可以指定某个类的某个方法在什么情况下返回什么样的值。
org.mockito.Mockito:
● when(...).thenReturn(...)
● doReturn(...). when(class).method(...)
大多情况下,上面2种方法通用,建议优先使用when-thenReturn,因为其可读性较高且会检查回传值的类型(Type-Safe Check):
1 List<String> list = mock(List.class);
2 when(list.get(100)).thenReturn("33"); //返回字符串,正常
3 when(list.get(0)).thenReturn(33); //返回数字,IDE报错,进行了type-safety的检查
4 doReturn(33).when(list).get(0); //IDE没报错,没进行type-safety的检查
并且对于Spy对象,when-thenReturn会真实调用方法,只是返回时返回设定的值,而doReturn根本不会调用实际的方法:
1 public boolean exist(String telOrigin) {
2 int i = 1/0;
3 return Optional.ofNullable(mapper.selectByTelOrigin(telOrigin)).isPresent();
4 }
1 @Test
2 @Rollback
3 public void save() {
4 BusinessAgent businessAgent = new BusinessAgent();
5 businessAgent.setTelOrigin("13888888888");
6 businessAgent.setAgentId(1);
7 doReturn(true).when(businessAgentRepository).exist("13888888888");
8// when(businessAgentRepository.exist("13888888888")).thenReturn(true);
9 int agentId = businessAgentRepository.save(businessAgent);
10 verify(businessAgentExtMapper).updateByPrimaryKeySelective(businessAgent);
11 verify(businessAgentExtMapper, never()).insert(businessAgent);
12 assertThat(agentId, equalTo(businessAgent.getAgentId()));
13 ……
14 }
比如上述代码,对于被测试类中存在1/0的非法算数运算,运行时汇报异常,使用when-thenReturn时测试无法通过,报算数异常,但是doReturn时测试可以正常通过,说明其根本没有调用实际方法。
org.mockito.BDDMockito:
● given(...).willReturn(...)
更符合BDD开发的习惯,Given…When…Then…实际上就是设定场景的状态、适用的事件,以及场景的执行结果,如:
1given(businessHousePic.getHouseId()).willReturn(3L);
3.3.3 调用方法
调用被测对象的方法,获取返回值。
3.3.4 验证结果
对有返回值的方法,验证返回数据是否和期望匹配。这里用到了2个方法verify()和assertThat():
1/**验证businessHousePicRepository调用了方法invalid