【JAVA】单元测试工具-Mockito学习心得

前一段时间换到了一个新的项目,开始疯狂加班,连学习都暂时停滞了。好在新的项目有新的要求,也经常会遇上新的问题,和新的技术,就算是在加班,也不失为一种学习的过程。

在原本的项目中我是从来不写单元测试的,第一是没有人做此要求,第二是我不会。一个java开发还没有研究过单元测试,说不定会笑死人,但是因为从来用不上,我也就从没有意识到自己需要补充这方面的知识。但是在新的工作环境中, 总是会遇到新的问题,这种被动的也能接触到很多新的知识。而我在工作之余对这些知识点进行扩展和运用。

一、Mockito的介绍

以下内容引用自博客《5分钟了解Mockito》

1. 什么是mock测试?什么是mock对象?

先来看看下面这个示例:

【JAVA】单元测试工具-Mockito学习心得_第1张图片

从上图可以看出如果我们要对A进行测试,那么就要先把整个依赖树构建出来,也就是BCDE的实例。

 

一种替代方案就是使用mocks

【JAVA】单元测试工具-Mockito学习心得_第2张图片

从图中可以清晰的看出

mock对象就是在调试期间用来作为真实对象的替代品。

mock测试就是在测试过程中,对那些不容易构建的对象用一个虚拟对象来代替测试的方法就叫mock测试。

知道什么是mock测试后,那么我们就来认识一下mock框架---Mockito

2. 什么是Mockito?

除了有一个好记的名字外,Mockito尝试用不一样的方法做mocking测试,是简单轻量级能够替代EasyMock的框架。使用简单,测试代码可读性高,丰富的文档包含在javadoc中,直接在IDE中可查看文档,实例,说明。更多信息:http://code.google.com/p/mockito/

3. stub和mock

相同点:Stub和Mock对象都是用来模拟外部依赖,使我们能控制。

不同点:而stub完全是模拟一个外部依赖,用来提供测试时所需要的测试数据。而mock对象用来判断测试是否能通过,也就是用来验证测试中依赖对象间的交互能否达到预期。在mocking框架中mock对象可以同时作为stub和mock对象使用,两者并没有严格区别。 更多信息:http://martinfowler.com/articles/mocksArentStubs.html

 

二、Mockito在单测中的使用

1. 使用Maven引入Mockito

在pom.xml文件中直接加入以下依赖,就可以使用Mockito进行单元测试了。

        
            org.mockito
            mockito-all
            test
        
        
            junit
            junit
            test
        

2. Mockito的重要注解

2.1 @Mock

很多情况下,测试一个功能需要依赖许多其他类的返回值,如果其他类没有好,这个类的功能就不能测试。

使用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.

【JAVA】单元测试工具-Mockito学习心得_第3张图片

但是如果我们不想过多的依赖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 没有成功。

【JAVA】单元测试工具-Mockito学习心得_第4张图片

结果返回确实是80,通过这样的方法mock数据是可行的。(必须注意的是在跑mock方法之前

那么又不得不提到另外一种场景了,我们不是想要mock UserInfoManager的返回值,而是想要mock UserManager的返回值,这样userInfoManager的代码还照常跑,这样才能验证UserInfoManager中代码的逻辑。

那么我们需要用到下一个很重要的注解@InjectMocks。

2.2 @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”。

【JAVA】单元测试工具-Mockito学习心得_第5张图片

代码通过,说明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。

2.3 @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,想用真实方法的时候使用真实方法。

【JAVA】单元测试工具-Mockito学习心得_第6张图片

结果代码通过了。

这个时候我新加一个类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的这个依赖是空的。

【JAVA】单元测试工具-Mockito学习心得_第7张图片

难道我们要在外面@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使用。这个就不举例说明了。

3. 常用方法

3.1  Mockito.when(classObject.function()).thenReturn(Obj) 

用来mock返回值上面已经提过了。

3.2 Mockito.doReturn(obj).when(classObject).function()

同样是用来模拟数据,但是和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");
    }
}

【JAVA】单元测试工具-Mockito学习心得_第8张图片

如图可见:在进行when().thenReturn()的时候,我们调用了真的方法。但是在doReturn().when()方法的时候,我们并没有真的执行getUserDO(),而是直接创建了返回值。

3.3 Mockito.doNothing().when(classObject).function()

用于模拟无返回值的方法调用。比如我们的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了,就不会执行方法里面的代码,也就不会抛出异常了。

3.4 verify()

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");
    }

 

可以验证被注入userInfoManager的userManager里的getUserDO()方法被调用了两次。

你可能感兴趣的:(单元测试,java,基础)