前一段时间换到了一个新的项目,开始疯狂加班,连学习都暂时停滞了。好在新的项目有新的要求,也经常会遇上新的问题,和新的技术,就算是在加班,也不失为一种学习的过程。
在原本的项目中我是从来不写单元测试的,第一是没有人做此要求,第二是我不会。一个java开发还没有研究过单元测试,说不定会笑死人,但是因为从来用不上,我也就从没有意识到自己需要补充这方面的知识。但是在新的工作环境中, 总是会遇到新的问题,这种被动的也能接触到很多新的知识。而我在工作之余对这些知识点进行扩展和运用。
以下内容引用自博客《5分钟了解Mockito》
先来看看下面这个示例:
从上图可以看出如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。
一种替代方案就是使用mocks
从图中可以清晰的看出
mock对象就是在调试期间用来作为真实对象的替代品。
mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。
知道什么是mock测试后,那么我们就来认识一下mock框架---Mockito
除了有一个好记的名字外,Mockito尝试用不一样的方法做mocking测试,是简单轻量级能够替代EasyMock的框架。使用简单,测试代码可读性高,丰富的文档包含在javadoc中,直接在IDE中可查看文档,实例,说明。更多信息:http://code.google.com/p/mockito/
相同点:Stub和Mock对象都是用来模拟外部依赖,使我们能控制。
不同点:而stub完全是模拟一个外部依赖,用来提供测试时所需要的测试数据。而mock对象用来判断测试是否能通过,也就是用来验证测试中依赖对象间的交互能否达到预期。在mocking框架中mock对象可以同时作为stub和mock对象使用,两者并没有严格区别。 更多信息:http://martinfowler.com/articles/mocksArentStubs.html
在pom.xml文件中直接加入以下依赖,就可以使用Mockito进行单元测试了。
org.mockito
mockito-all
test
junit
junit
test
很多情况下,测试一个功能需要依赖许多其他类的返回值,如果其他类没有好,这个类的功能就不能测试。
使用Mockito的功能,我们可以让单元测试脱离对其他类的依赖,如果我们需要其他类返回一个期望的返回值,我们可以直接mock这个返回值,让测试可以很顺利的继续下去,这样测试就能专注于单元内。
@Mock注解能让你仿造一个依赖,然后通过Mockito.when(class.funciton()).thenReturn(Object)这样的格式来模拟返回值。
根据一个例子我们来尝试一下,先新建一个springboot的project,然后建三个类:
UserDO
public class UserDO {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
UserManager
import org.springframework.stereotype.Service;
@Service
public class UserManager {
public UserDO getUserDO() {
UserDO user = new UserDO();
user.setName("real");
user.setAge(20);
return user;
}
}
UserInfoManager
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserInfoManager {
@Autowired
private UserManager userManager;
public String getUserName() {
return userManager.getUserDO().getName();
}
public Integer getUserAge() {
return userManager.getUserDO().getAge();
}
}
根据上面的代码我们可以看出UserDO是一个实体类,UserManager和UserInfoManager都是处理业务的service类,而其中UserInfoManager依赖了UserManager。
此时我们想要对UserInfoManager进行单元测试可以,在src/test包里写测试类,首先我们要保证依赖注入,所以我们要对测试类添加注解:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { JavaLearn04MockitoApplicationTests.class })
并把JavaLearn04MockitoApplicationTests.class中的注解@SpringBootTest替换为@SpringBootApplication,让springboot真的跑起来,把上面的类都加载进来,不然会报这个错:
Error creating bean with name 'com.mrh.java.mockito.UserInfoManagerTest': Unsatisfied dependency expressed through field 'userInfoManager';
为了方便我们把应该注解在测试类上的注解,注解到一个BaseTest上,这样之后对所有类的测试类都可以继承这个基类,这样就不用麻烦的一个一个加注解了。
@RunWith(SpringRunner.class)
@SpringBootApplication
public class JavaLearn04MockitoApplicationTests {
@Test
public void contextLoads() {
}
}
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.PropertySource;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { JavaLearn04MockitoApplicationTests.class })
@PropertySource(value = { "classpath:application.properties" })
public class BaseTest {
}
到此为止,我们可以开始认真的写测试类了:
UserInfoManagerTest
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
public class UserInfoManagerTest extends BaseTest {
@Autowired
private UserInfoManager userInfoManager;
@Test
public void getUserAge() {
Assert.assertTrue(userInfoManager.getUserAge().equals(20));
}
}
我们预测获取的age数值为20,不然就是错的。
而运行结果,果然为20.
但是如果我们不想过多的依赖userManager的完成度(比如userManager的某个方法还没完成,或者目前还没调通),而给userManger的方法一个伪造的预期结果,这样这个单元测试就可以独立的测试UserInfoManager的逻辑,而不会受到userManager的影响了,应该怎么办呢?
这就是@Mock方法可以大显身手的时候了。
从上面的userInfoManager代码中看出,我们返回的UserDO的age是20,我们在这段代码里使用@Mock注解注入了UserInfoManager,这样我们就可以通过上面提到过的方法来mock一个预期的返回值。
public class UserInfoManagerTest extends BaseTest {
@Mock
private UserInfoManager userInfoManager;
@Test
public void getUserAger() {
Mockito.when(userInfoManager.getUserAge()).thenReturn(80);
Assert.assertTrue(userInfoManager.getUserAge().equals(80));
}
}
这段代码只有当getUserAge()方法返回80的时候才会通过,如果不是80,说明我们的mock 没有成功。
结果返回确实是80,通过这样的方法mock数据是可行的。(必须注意的是在跑mock方法之前
那么又不得不提到另外一种场景了,我们不是想要mock UserInfoManager的返回值,而是想要mock UserManager的返回值,这样userInfoManager的代码还照常跑,这样才能验证UserInfoManager中代码的逻辑。
那么我们需要用到下一个很重要的注解@InjectMocks。
这个注解的作用是创建一个实例,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
使用这个注解可以创建一个实例,并且将你用@Mock注解的对象注入到这个实例中。
用这个注解我们就可以是使用@Mock创建mock的UserManager对象,并用@InjectMocks注解创建一个UserInfoManager的实例将UserManager对象注入其中。
必须提到的是使用这个注解,我们在单测方法最前面一定要执行以下语句:
MockitoAnnotations.initMocks(this);
否则无法将Mock注入。
public class UserInfoManagerTest extends BaseTest {
@Mock
private UserManager userManager;
@InjectMocks
private UserInfoManager userInfoManager;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@After
public void after() {
Mockito.reset(userManager);
}
@Test
public void getUserName() {
UserDO user = new UserDO();
user.setName("mock");
Mockito.when(userManager.getUserDO()).thenReturn(user);
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
}
}
从之前的代码我们可以看出来如果我们真的调用UserManager的getUserDO,返回的对象里的name应该是“real”,而我们这里预期mock成功后,返回的应该是我们的mock数据“mock”。
代码通过,说明mock的依赖正确注入了。
可以看到我们通过@Mock和@InjectMocks,我们可以对返回值和依赖的返回值进行mock,但是下面的代码却出了一点问题:
public class UserInfoManagerTest extends BaseTest {
@Mock
private UserManager userManager;
@InjectMocks
private UserInfoManager userInfoManager;
// @Mock
// private UserInfoManager userInfoManager;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@After
public void after() {
Mockito.reset(userManager);
}
@Test
public void getUserAge() {
Assert.assertTrue(userInfoManager.getUserAge().equals(20));
}
@Test
public void getUserName() {
UserDO user = new UserDO();
user.setName("mock");
Mockito.when(userManager.getUserDO()).thenReturn(user);
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
}
}
当我想在getUserAge的Test里面使用UserManager真方法,在getUserName的Test里使用mock的返回值时就遇到了问题,虽然我在getUserAge的Test里面并没有对UserManager进行mock,但是他也终究不是一个真的依赖,不能返回真的结果了, 只能返回null。运行结果如下图:
如果我们想对依赖做局部mock,想要的时候就mock它,不想要的时候就运行真的方法应该如何呢?
接下来就要介绍能让这个构想实现的下一个注解:@Spy。
Mockito中的Mock和Spy都可用于拦截那些尚未实现或不期望被真实调用的对象和方法,并为其设置自定义行为。二者的区别在于:
1、Mock声明的对象,对函数的调用均执行mock(即虚假函数),不执行真正部分。
2、Spy声明的对象,对函数的调用均执行真正部分。
于是我们对自己的代码做一下调整:
public class UserInfoManagerTest extends BaseTest {
@Spy
private UserManager userManager;
@InjectMocks
private UserInfoManager userInfoManager;
// @Mock
// private UserInfoManager userInfoManager;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@After
public void after() {
Mockito.reset(userManager);
}
@Test
public void getUserAge() {
Assert.assertTrue(userInfoManager.getUserAge().equals(20));
}
@Test
public void getUserName() {
UserDO user = new UserDO();
user.setName("mock");
Mockito.when(userManager.getUserDO()).thenReturn(user);
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
}
}
用@Spy注解后的方法,可以在想mock的使用mock,想用真实方法的时候使用真实方法。
结果代码通过了。
这个时候我新加一个类OrgManager。
import org.springframework.stereotype.Service;
@Service
public class OrgManager {
public String getOrgName() {
return "abc company";
}
}
然后修改UserInfoManager增加一个依赖OrgManager的方法:
@Service
public class UserInfoManager {
@Autowired
private UserManager userManager;
@Autowired
private OrgManager orgManager;
public String getUserName() {
return userManager.getUserDO().getName();
}
public Integer getUserAge() {
return userManager.getUserDO().getAge();
}
public String getOrgName() {
return orgManager.getOrgName();
}
}
这个时候我们在测试类里对这个新方法进行测试:
原因正是因为我们使用@InjectMocks注入了UserManager,但是OrgManager并没有被注入,所以实际上进入方法的时候UserInfoManager的这个依赖是空的。
难道我们要在外面@Spy一个OrgManager然后和UserManager一起注入吗?但是我们并不想对这个类进行mock,如果在依赖多的情况下这样做非常繁琐,那我们能不能也增加@Spy注解UserInfoManager这样,当我们注入@Spy对象的时候使用@Spy对象,否则使用真对象呢?(不确定@Spy有这个功能)。
@InjectMocks
@Spy
private UserInfoManager userInfoManager;
测试后发现@Spy对象能执行真实代码,但是并不能为对象注入依赖。
而@InjectMocks是可以和@Autowired一起使用的,我们可以两个个注解叠加使用,让@Autowired注解为对象注入真依赖。
@InjectMocks
@Autowired
private UserInfoManager userInfoManager;
下图可见这样的方法是可以通过的。
现在还有第三种场景,如果我的依赖的对象还有依赖(这在大多数的项目中是非常常见的),根据上面的说法用@Spy的对象是不会自动注入依赖的,此时怎么处理呢?
我们可以叠加@Spy和@Autowired使用。这个就不举例说明了。
用来mock返回值上面已经提过了。
同样是用来模拟数据,但是和3.1 不同的是,3.1会真的调用方法,而3.2不会。
修改之前的测试类来验证一下:
public class UserInfoManagerTest extends BaseTest {
@Spy
private UserManager userManager;
@InjectMocks
@Autowired
private UserInfoManager userInfoManager;
@Before
public void before() {
MockitoAnnotations.initMocks(this);
}
@After
public void after() {
Mockito.reset(userManager);
}
@Test
public void getUserName() {
System.out.println("run getUserName");
UserDO user = new UserDO();
user.setName("mock");
Mockito.when(userManager.getUserDO()).thenReturn(user);
System.out.println("start getUserName test");
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Mockito.verify(userManager, times(2)).getUserDO();
System.out.println("end getUserName test");
}
@Test
public void getUserName2() {
System.out.println("run getUserName2");
UserDO user = new UserDO();
user.setName("mock");
Mockito.doReturn(user).when(userManager).getUserDO();
System.out.println("start getUserName2 test");
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Mockito.verify(userManager, times(2)).getUserDO();
System.out.println("end getUserName2 test");
}
}
如图可见:在进行when().thenReturn()的时候,我们调用了真的方法。但是在doReturn().when()方法的时候,我们并没有真的执行getUserDO(),而是直接创建了返回值。
用于模拟无返回值的方法调用。比如我们的function实际跑是跑不通的(报错或没完成),用这个方法就可以让代码直接模拟他成功执行。
比如我们调整之前的代码:
UserManager:
@Service
public class UserManager {
public UserDO getUserDO() {
UserDO user = new UserDO();
user.setName("real");
user.setAge(20);
System.out.println("run getUserDO");
return user;
}
public void unfinishedFunciotn() throws Exception {
throw new Exception();
}
}
UserInfoManager:
@Service
public class UserInfoManager {
@Autowired
private UserManager userManager;
@Autowired
private OrgManager orgManager;
public String getUserName() {
return userManager.getUserDO().getName();
}
public Integer getUserAge() {
return userManager.getUserDO().getAge();
}
public String getOrgName() {
return orgManager.getOrgName();
}
public boolean unfinishedFunciotn() throws Exception {
userManager.unfinishedFunciotn();
return true;
}
}
可以看出新加的方法是一定会抛出异常的,但是如果我们想让unfinishedFunciotn()返回true,应该如何mock呢?
@Test
public void unfinishedFunciotn() throws Exception {
Mockito.doNothing().when(userManager).unfinishedFunciotn();
Assert.assertTrue(userInfoManager.unfinishedFunciotn());
}
如图所示,只要用这个方法mock了,就不会执行方法里面的代码,也就不会抛出异常了。
verify方法用于验证方法总共被调用了几次,且只能验证mock的方法。
@Test
public void getUserName2() {
System.out.println("run getUserName2");
UserDO user = new UserDO();
user.setName("mock");
Mockito.doReturn(user).when(userManager).getUserDO();
System.out.println("start getUserName2 test");
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Assert.assertTrue(userInfoManager.getUserName().equals("mock"));
Mockito.verify(userManager, times(2)).getUserDO();
System.out.println("end getUserName2 test");
}