单元测试

2018/06/13 basis

1. Mockito

单元测试开发中,我们经常会遇到测试的类有很多依赖的类、对象、资源,从而形成巨大的依赖树,mock可以模拟外部依赖,适应单元测试。

比如我们在开发中很容易出现这种情况:

(a)依赖(b)依赖(c)依赖(d)

这种情况下单元测试就变得极其复杂,而且如果需要测试数据库或网络请求返回值的情况,就更加复杂了。我们总不至于去修改真实数据库,去修改真实的网络情况吧。

但是如果我们引入Mockito情况就会变得简单许多,我们可以mock虚拟的依赖,并可以轻松的修改返回值甚至抛出异常:

(a)依赖(mock b)

1.1 Mockito 的简单用法

Mockito是一个功能非常丰富的框架,下面介绍几个典型和常用的方法,详细的使用可以到官网上查询。

Mockito官网

Mocktio文档

我们在非Spring Boot项目中使用Mockito需要手动引入依赖,Spring Boot项目中Mocktio的使用会在下面介绍。

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

1.1.1 mock对象

// 模拟LinkedList 的一个对象  
LinkedList mockedList = mock(LinkedList.class);
// 此时调用get方法,会返回null,因为还没有对方法调用的返回值做模拟
System.out.println(mockedList.get(0));

1.1.2 方法调用的返回值

// 模拟获取第一个元素时,返回字符串first。  给特定的方法调用返回固定值在官方说法中称为stub。
when(mockedList.get(0)).thenReturn("first");
// 此时打印输出first
System.out.println(mockedList.get(0));

1.1.3 方法调用抛出异常

// 模拟获取第二个元素时,抛出RuntimeException  
when(mockedList.get(1)).thenThrow(new RuntimeException());
// 此时将会抛出RuntimeException  
System.out.println(mockedList.get(1));

如果一个方法没有返回值类型,那么可以使用此方法模拟异常抛出

doThrow(new RuntimeException("clear exception")).when(mockedList).clear();
mockedList.clear();

1.1.4 调用方法时的参数匹配

// anyInt()匹配任何int参数,这意味着参数为任意值,其返回值均是element  
when(mockedList.get(anyInt())).thenReturn("element");
// 此时打印是element
System.out.println(mockedList.get(999));

1.1.5 验证方法调用次数

// 调用add一次
mockedList.add("once");
// 下面两个写法验证效果一样,均验证add方法是否被调用了一次
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

1.1.6 校验行为

校验方法调用

// mock creation
List mockedList = mock(List.class);
// using mock object
mockedList.add("one");
mockedList.clear();
// verification
verify(mockedList).add("one");
verify(mockedList).clear();

校验方法调用顺序

// A. Single mock whose methods must be invoked in a particular order
List singleMock = mock(List.class);
//using a single mock
singleMock.add("was added first");
singleMock.add("was added second");
//create an inOrder verifier for a single mock
InOrder inOrder = inOrder(singleMock);
//following will make sure that add is first called with "was added first, then with "was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
// B. Multiple mocks that must be used in a particular order
List firstMock = mock(List.class);
List secondMock = mock(List.class);
//using mocks
firstMock.add("was called first");
secondMock.add("was called second");
//create inOrder object passing any mocks that need to be verified in order
InOrder inOrder = inOrder(firstMock, secondMock);
//following will make sure that firstMock was called before secondMock
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
// Oh, and A + B can be mixed together at will

校验方法是否从未调用

//using mocks - only mockOne is interacted
mockOne.add("one");
//ordinary verification
verify(mockOne).add("one");
//verify that method was never called on a mock
verify(mockOne, never()).add("two");
//verify that other mocks were not interacted
verifyZeroInteractions(mockTwo, mockThree);

1.2 Mockito 在 Spring Boot 中的使用

上面我们提到了非 Spring Boot 需要手动引入 Mocktio 的依赖,但在 Spring Boot 项目中,Spring Boot Test 默认依赖 Mocktio ,所以只要依赖了 Spring Boot Test 就不再需要我们手动引入依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

一般情况下,单元测试不需要 Spring 的启动。此时我们按下面的写法:

import static org.mockito.Mockito.*;

public class MyServiceTest {

    /**被单元测试的类*/
    @InjectMocks
    private MyServiceImpl myServiceImpl;

    /**被单元测试的类中所有的依赖*/
    @Mock
    private MyMapper myMapper;

    /**在单元测试之前,执行Mockito的注解扫描,并注入依赖*/
    @Before
    public void setUp() { MockitoAnnotations.initMocks(this); }

    @Test
    public void myServiceMethod() {
        myServiceImpl.myServiceMethod();
    }
}

或者通过注解 RunWith 的方式:

import static org.mockito.Mockito.*;

@RunWith(SpringJUnit4ClassRunner.class)
public class MyServiceTest {

    /**被单元测试的类*/
    @InjectMocks
    private MyServiceImpl myServiceImpl;

    /**被单元测试的类中所有的依赖*/
    @Mock
    private MyMapper myMapper;

    @Test
    public void myServiceMethod() {
        myServiceImpl.myServiceMethod();
    }
}

但有些情况下,我们可能需要 Spring 启动着。可以按下面这样写:

import static org.mockito.Mockito.*;

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class MyServiceTest {

    /**被单元测试的类*/
    @InjectMocks
    private MyServiceImpl myServiceImpl;

    /**被单元测试的类中所有的依赖*/
    @Mock
    private MyMapper myMapper;

    @Test
    public void myServiceMethod() {
        myServiceImpl.myServiceMethod();
    }
}

2. PowerMock

Mocktio 是一款轻巧方便的 mock 工具,能够完成 90% 的单元测试要求,但是确无法完成静态方法、构造方法、私有方法、final 方法以及系统方法的模拟。因此我们在碰到那 10% 的无法完成时引入了 PowerMock 。PowerMock 并不是对 Mocktio 的替代,而是补充和完善。PowerMock 有多个版本,分别是对多个 mock 工具的补充,此处我们仅介绍 PowerMock 的 Mocktio 和 Junit4 版本,其余版本类似。

PowerMock 官网

首先引入依赖:

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <scope>test</scope>
</dependency>

此处有一点需要注意,PowerMock 的版本与 Mocktio 的版本是对应的,如果用错了版本,会出现一些莫名其妙的错误。

PowerMock 与 Mockito 版本

Mockito PowerMock
2.8.0-2.8.9 1.7.x
2.7.5 1.7.0RC4
2.4.0 1.7.0RC2
2.0.0-beta - 2.0.42-beta 1.6.5-1.7.0RC
1.10.8 - 1.10.x 1.6.2 - 2.0
1.9.5-rc1 - 1.9.5 1.5.0 - 1.5.6
1.9.0-rc1 & 1.9.0 1.4.10 - 1.4.12
1.8.5 1.3.9 - 1.4.9
1.8.4 1.3.7 & 1.3.8
1.8.3 1.3.6
1.8.1 & 1.8.2 1.3.5
1.8 1.3
1.7 1.2.5

2.1 PowerMock 的简单用法

具体使用与 Mocktio 在 SpringBoot 中类似,如下:

import static org.mockito.Matchers.*;
import static org.powermock.api.mockito.PowerMockito.*;

//如果需要spring运行,则使用此注解
//@SpringBootTest
//配置PowerMock的启动器,并配置代理原先的SpringJUnit4ClassRunner启动器
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)
//配置需要PowerMock进行mock的类
@PrepareForTest(value = {StringUtils.class})
//配置PowerMock的classLoader忽略该路径下的类
@PowerMockIgnore({"javax.management.*"})
public class MyServiceTest {

    /**被单元测试的类*/
    @InjectMocks
    private MyServiceImpl myServiceImpl;

    /**被单元测试的类中所有的依赖*/
    @Mock
    private MyMapper myMapper;

    @Test
    public void myServiceMethod() {
        mockStatic(StringUtils.class);
        when(StringUtils.isEmpty(any())).thenReturn(true);

        myServiceImpl.myServiceMethod();
    }
}
  1. 我们依然使用 org.mockito.Matchers 下的 any() 等方法进行匹配,但是 when()thenReturn() 等方法就得使用 PowerMock 的方法了。
  2. 配置 RunWith 使用 PowerMockRunner 代理原来的 SpringJUnit4ClassRunner ,因为 PowerMock 需要在运行之前做一些处理。
  3. 通过 PrepareForTest 指定需要 PowerMock 进行特殊 mock 的类。例如,你想 mock 的静态方法、构造方法、私有方法、final 方法以及系统方法所在的类。
  4. 通过 PowerMockIgnore 配置需要 PowerMock 的 classLoader 忽略的类,因为PowerMock 为了实现这些特殊方法的 mock ,需要借助自定义的 classLoader 。

2.1.1 mock 方法内部 new 出来的对象

目标代码:

public class MyServiceImpl {

    public file createFile(String path){
        return new File(path);
    }
}

测试代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest(MyServiceImpl.class)
public class MyServiceImplTest {

    @Test
    public void createFile() {
        File file = new File;
	    PowerMockito.whenNew(File.class).withArguments("abc").thenReturn(file);

        MyServiceImpl myService = new MyServiceImpl();
        Assert.isTrue(myService.createFile("abc") == file);
    }
}

2.1.2 mock 普通类的静态方法

目标代码:

public class ListUtil {

    public static boolean isEmpty(List list) {
        return list.isEmpty();
    }
}

测试代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest(ListUtil.class)
public class ListUtilTest {

    @Test
    public void isEmpty(){
        PowerMockito.mockStatic(ListUtil.class);
        PowerMockito.when(ListUtil.isEmpty(any())).thenReturn(false);

        List list = new ArrayList();
        Assert.isTrue(!ListUtil.isEmpty(list));
    }
}

2.1.3 mock 私有方法

目标代码:

public class MyServiceImpl {

    public String add(String a, String b) {
        return stringToInt(a) + stringToInt(b) + "";
    }

    private int stringToInt(String str) {
        return Integer.valueOf(str);
    }
}

测试代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest(MyServiceImpl.class)
public class MyServiceImplTest {

    @Test
    public void createFile() {
        MyServiceImpl myService = PowerMockito.mock(MyServiceImpl.class);
        PowerMockito.when(myService, "stringToInt").thenReturn(0);
        PowerMockito.when(myService.add(any(), any())).thenCallRealMethod();

        Assert.isTrue(myService.add(null));
    }
}

Search

    Table of Contents