使用Mockito和SpringTest进行单元测试
单元测试的作用以及重要性其实不用在这里不断的重复了.为了保证软件的可靠性,单元测试可能是最容易执行的一个可靠的手段了.一方面,程序员通过编写单元测试来验证自己程序的有效性,另外一方面,管理者通过持续自动的执行单元测试和分析单元测试的覆盖率等来确保软件本身的质量.
JAVA生态圈里面说起单元测试一般都会使用JUnit
或者TestNG
.其中JUnit
可能使用的更加频繁一些,JUnit4
使用注解以及各种其他框架对它的支持,形成了一个完善的单元测试的生态圈.
SpringTest单元测试
现在的JAVA WEB项目中,起码一半以上的项目是使用了Spring的.因此,单纯的使用JUnit
来进行单元测试并不是十分的好用,对于由Spring管理的Bean
要进行单元测试,首先需要实例化Spring上下文,然后又需要手动的去注入依赖的Bean
,比较麻烦.特别是对于有事务的单元测试,或数据库数据测试,单独使用JUnit
几乎无法完成.
所幸,SpringFramework也意识到了这点,于是推出了spring-test
模块,他能完成Spring环境与Junit单元测试环境的对接.让我们只专注于单元测试本身进行书写,而由它来完成Spring容器的初始化、Bean
的获取、数据库事务的管理、数据操作正确性检查等等。
项目中引用spring-test
要在项目中使用Spring测试框架非常的简单,在Maven中依赖spring-test
即可:
|
|
Maven会自动的完成依赖包的引用。
创建单元测试类
Spring-test以及Junit4都推荐使用注解的方式来进行配置。因此,我们要进行单元测试,需要的就是创建一个自己的测试类。然后在类上面进行少许的注解,表明我们要如何初始化spring,需要引用spring的哪些配置,然后再指明需要在单元测试类中注入哪些bean即可,比如:
|
|
从这个简单的例子当中,我们可以看到和单独使用JUnit有几个不一样的地方。
- 首先使用了
@RunWith(SpringJUnit4ClassRunner.class)
来告诉JUnit
,需要结合Spring-test来执行单元测试 - 然后使用
@ContextConfiguration
注解来告诉Spring-test
这个单元测试最小需要依赖的spring的上下文是什么,因为通常单元测试不需要引入所有的spring配置,因此我们可以在这里可选的读取几个需要的配置即可 - 而后,我们可以在类上或者方法上使用
@Transactional
注解来标识某个或某些单元测试用例需要使用事务,并且可能会进行回滚,防止单元测试引起数据库脏数据。 - 同时,我们可以在这个单元测试类中直接使用
@Autowired
以及@Qualifier
注解直接注入经过Spring管理过的依赖Bean。而我们测试的主要目的也就是对这些bean进行单元测试。 - 剩下的就和单独的使用JUnit进行单元测试是一样的了。
经过以上的处理,我们就可以进行spring框架下的单元测试了,而且基本上能满足80%以上的需求。更多更高级的用法,可以参考:JUnit官网以及Spring-test官网
基于数据库数据的单元测试数据准备
其实这个和要讲的Spring-test没有必然的联系。但是在很多的情况下,我们都会对数据库访问层进行单元测试。那么,往往就涉及到了数据库的数据打桩。为了能实现单元测试的自动化和可重复化,我们可以把桩数据写入一个单元测试的SQL中,在每次执行单元测试的时候,先执行这个SQL,给数据库准备数据,然后再执行单元测试。而这一切,我们可以写一个测试的基类来进行处理(JDK1.8中允许给接口增加默认方法,因此,这个地方我们可以定义一个接口来实现这个功能):
|
|
有了这个接口,我们在写单元测试类的时候,就可以直接实现这个接口,然后在@Before
方法中调用prepareRmdbData
方法来初始化数据库:
|
|
然后把SQL文件命名为TestPoolService.sql
,放入test/resources
中即可。
Mock测试
经过上面所说的JUnit
+SpringTest
,基本上可以满足80%的单元测试了。但是,由于现在的系统越来越复杂,相互之间的依赖越来越多。特别是微服务化以后的系统,往往一个模块的代码需要依赖几个其他模块的东西。因此,在做单元测试的时候,往往很难构造出需要的依赖。一个单元测试,我们只关心一个小的功能,但是为了这个小的功能能跑起来,可能需要依赖一堆其他的东西,这就导致了单元测试无法进行。所以,我们就需要再测试过程中引入Mock
测试。
所谓的Mock
测试就是在测试过程中,对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。
比如有一段代码的依赖为:
当我们要进行单元测试的时候,就需要给A
注入B
和C
,但是C
又依赖了D
,D
又依赖了E
。这就导致了,A的单元测试很难得进行。
但是,当我们使用了Mock来进行模拟对象后,我们就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockB
和MockC
指定一个明确的行为。就像这样:
因此,当我们使用Mock后,对于那些难以构建的对象,就变成了个模拟对象,只需要提前的做Stubbing
(桩)即可,所谓做桩数据,也就是告诉Mock对象,当与之交互时执行何种行为过程。比如当调用B对象的b()方法时,我们期望返回一个true
,这就是一个设置桩数据的预期。
Mockito简单入门
在JAVA中,Mock测试框架主要有Mockito,Jmock,EsayMock,PowerMock等等。其中Mockito
最为方便和简单,用的人也最多。而PowerMock
是对Mockito
的一个增强,增加了对静态、final、私有方法的Mock,但是基本用法和Mockito
大致相同。对因此,我们使用Mockito
作为Mock的框架。
项目中引用Mockito
要在项目中使用Mockito
非常的简单,只需要在项目的Maven中引入:
|
|
使用Mockito进行测试
我们这里使用一个最简单的用户基本信息管理来做演示。这个功能有一个模型对象UserPO
,一个数据库访问层UserDao
,一个服务层UserService
。
UserPO
|
|
UserDao
|
|
UserService
|
|
创建单元测试类
当我们准备好上面的例子后,就可以开始创建单元测试类了。
在这里,我们假设IUserDao
是个很复杂的访问集群数据库的对象,并且这个类已经经过完整的测试保证是正确了的。而我们现在只需要单元测试UserService#updateUserName
这个方法。因此,就需要对IUserDao
进行Mock并打桩。
|
|
上面的代码就是一个完整的单元测试类,有两个用例,分别验证当用户存在能修改名字的情况以及用户不存在修改名字失败的情况。
我们从头来分析一下这个单元测试类。
标明需要Mock的对象
程序一来,先定义了被测试的对象实例userService
以及需要被模拟的IUserDao
对象。需要注意的是,我们在userDao
成员变量上增加了一个@Mock
注解。这个注解的作用就是告诉Mockito,这个对象是需要被Mock的。
接着,我们创建了一个setUp()
方法,并使用了JUnit
的注解@Before
,用于在执行单元测试前执行一些代码,我们在这里需要对Mock的对象进行打桩。
MockitoAnnotations.initMocks(this);
这句话就是对所有标注了@Mock
注解的对象进行模拟。当然,我们也可以不使用注解,而直接使用代码的方式手动的初始化Mock的对象:
|
|
接着就是指定userDao的行为也就是桩了。这也是Mockito最常用最核心的方法了。
|
|
Mockito最基本的用法就是调用 when
以及thenReturn
方法了。他们的作用就是指定当我们调用被代理的对象的某一个方法以及参数的时候,返回什么值。
比如第一句的
Mockito.when(userDao.getUserById(1L)).thenReturn(new UserPO(1L,"user1",20));
就表明,当我调用userDao.getUserById(1L)
的时候,这个方法返回new UserPO(1L,"user1",20)
这个实例。当我们需要返回NULL的时候,也非常的简单,直接写成
thenReturn(null)
即可。如果我们不关心调用的参数的入参,那么Mockito提供了几个方法来表示:
any()
、any(Class<T> type)
、anyBoolean()
、anyByte()
、anyChar()
、anyInt()
、anyLong()
、anyFloat()
、anyDouble()
、anyShort()
、anyString()
、anyList()
、anyListOf(Class<T> clazz)
、anySet()
、anyMap()
等等相反,Mockito还提供了很强大的入参过滤,用于指定只对某一些入参的调用进行Mock。比如:正则表达式
Mockito.matches(".*User$"))
、开头结尾验证endsWith(String suffix)
startsWith(String prefix)
、判空验证isNotNull()
isNull()
甚至,我们还可以自定义入参匹配:
argThat(ArgumentMatcher<T> matcher)
。ArgumentMatcher
只有一个方法boolean matches(T argument);
传入入参,返回一个boolean表示是否匹配。在JDK1.8中,我们可以使用lambda表达式来自定义入参匹配,比如:1Mockito.argThat(argument -> argument instanceof UserPO);除了我们期望调用一个方法后返回一个值外,有些时候,我们可能期望他抛出一个异常。这个时候,我们可以调用
thenThrow(Throwable... throwables);
用来抛出异常,这个方法有三个重载:thenThrow(Throwable... throwables)
: 直接指定抛出的异常实例thenThrow(Class<? extends Throwable> throwableType)
: 指定抛出异常的类型,执行的时候动态的实例化一个异常实例thenThrow(Class<? extends Throwable> toBeThrown, Class<? extends Throwable>... nextToBeThrown)
: 多次调用,依次抛出异常12//当调用userDao的更新时,如果传入的用户的名字是admin,那么就不允许修改,直接抛出异常Mockito.when(userDao.updateUser(Mockito.argThat(argument -> argument.getName().equals("admin")))).thenThrow(IllegalArgumentException.class);
此外,Mockito还提供了两个表示行为的方法:
thenAnswer(Answer<?> answer);
、thenCallRealMethod();
,分别表示自定义处理调用后的行为,以及调用真实的方法。这两个方法在有些测试用例中还是很有用的。对于同一个方法,Mockito可以是顺序与次数关心的。也就是说可以实现同一个方法,第一次调用返回一个值,第二次调用返回一个值,甚至第三次调用抛出异常等等。只需要连续的调用
thenXXXX
即可。最后,还有一个需要说明的就是如果为一个返回为Void的方法设置桩数据。上面的方法都是表示的是有返回值的方法,而由于一个方法没有返回值,因此我们不能调用
when
方法(编译器不允许)。因此,对于无返回值的方法,Mockito提供了一些列的doXXXXX
方法,比如:doAnswer(Answer answer)
、doNothing()
、doReturn(Object toBeReturned)
、doThrow(Class<? extends Throwable> toBeThrown)
、doCallRealMethod()
。他们的使用方法其实和上面的thenXXXX
是一样的,但是when
方法传入的是Mock的对象:12345/*对void的方法设置模拟*/Mockito.doAnswer(invocationOnMock -> {System.out.println("进入了Mock");return null;}).when(fileRecordDao).insert(Mockito.any());
验证Mock对象的调用
其实,一个最简单的Mock单元测试到这里已经算是完成了。我们已经验证了userService中的方法的正确性。但是,在复杂的方法调用堆栈中,往往可能出现结果正确,但是过程不正确的情况。比如,updateUserName
方法返回false是有两种可能的,一种可能是用户没有找到,还有一种可能就是userDao.updateUser(userPO)
返回false。因此,如果我们只是使用Assert.assertFalse(updated);
来验证结果,可能就会忽略某些错误。
Mockito同时提供了一些列的方法来对调用过程中的Mock对象的方法调用进行跟踪。我们可以对这些调用的过程进行断言验证,保证单元测试的结果与过程都是符合我们预期的。
|
|
Mockito.verify(userDao).getUserById(1L);
方法即验证了getUserById(1L)
这个方法是否被调用过,如果没有被调用过(包括入参要一致),就会抛出异常。- 除了最简单的
verify(T mock)
方法外,还提供了verify(T mock, VerificationMode mode)
方法。第二个参数有很多默认的实现,用于满足不同的需求。比如:Mockito.verify(userDao,Mockito.times(1)).getUserById(1L);
表示调用第一次 是传入的getUserById(1L);
,Mockito.verify(userDao,Mockito.times(2)).getUserById(2L);
表示调用第二次是传入的getUserById(2L);
,如果测试用例的调用顺序与参数不满足的话,就会报错。 - 除了
times
函数外,还提供了after
、atLeast
、only
、atMost
、timeout
等等。 verifyZeroInteractions
和verifyNoMoreInteractions
这两个方法的实现其实是一样的,只是名字不一样,作用就是验证被Mock的对象的所有被调用的方法是否都被Verify过了。这样就能保证调用没有被遗漏。当有方法被调用了,但是我们在测试用例中没有verify的话,那么调用这两个方法就会抛异常。
Mockito与SpringTest整合
经过前面的讲解,Mockito
的基本用法基本上应该都了解了。那么现在就需要整合Mockito
和SpringTest
了。
其实这两者的整合也非常的简单。和他们单独使用的时候并没有什么区别。
|
|
与单独使用Mockito相比,最大的不同其实就是在setUp()方法中调用的ReflectionTestUtils.setField(AopTargetUtils.getTarget(poolService), ”poolDao“,poolDao);
这个方法。ReflectionTestUtils是Spring-test提供的一个用于反射处理测试类的工具,通过这个,我们可以替换某一个被spring所管理的bean的成员变量。把他换成我们Mock出来的模拟对象。
当然这又引出了一个问题,就是如果依赖的对象的依赖对象需要被Mock,那么手动的不断重复的找需要被Mock的成员变量非常的麻烦。因此,我们可以写一个AbstractTestExecutionListener
监听器,当注入依赖的时候,找到被Mock的变量,以及需要被注入的变量,然后做关系的依赖。这样就能自动的对成员变量做替换了。
|
|
以上代码即为Mock初始化的监听器。它会查询这个测试用例的所有的成员变量。找到被标记为@Mock
的变量,然后模拟出来。而后,又找到所有被标注为@Autowired
的成员变量,判断变量类型是否是需要被Mock的。
当需要使用这个监听器的时候,只需要增加一个注解@TestExecutionListeners
即可:
|
|