Bootstrap

【学习笔记】单元测试之mockito学习笔记

Mockito库能够Mock对象、验证结果以及打桩(stubbing)。比如在测试时,可以用mockito模拟查询数据库的操作,即将查询数据库的方法拦截,并人工设置其返回值,这样就不用真正去数据库中拿取数据了。此外还可以对某个方法的运行结果进行验证等。

1. 相关包和依赖的导入

在使用mockito之前首先要导入包,建议采用静态导入:import static org.mockito.Mockito.*;,此外,还要在pom.xml文件中导入Junit相关依赖。

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
</dependency>

2. 验证方法是否执行

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    mockedList.add(1);
    // 验证是否执行了add()方法
    verify(mockedList).add(1);
    mockedList.clear();
    // 验证是否执行了clear()方法
    verify(mockedList).clear();
}

如果所验证的方法执行了,则不输出任何内容,反之,如果没有执行则会输出报错信息。

3. 测试桩

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    // 测试桩,当指定的语句执行后返回对应的值或抛异常
    when(mockedList.get(0)).thenReturn("first");       
    when(mockedList.get(1)).thenThrow(new RuntimeException());

    // 输出测试桩对应的内容first
    System.out.println(mockedList.get(0));
    // 输出测试桩对应的内容,抛异常
    System.out.println(mockedList.get(1));
}

其输出为 first和 RuntimeExceptioni类型的异常信息。

基于注解的方式

基于注解的方式可以让代码更加简介,也是最为推荐的。在使用基于注解的mock时,需要先让代码支持mock,有三种方式:

  • 在测试类上添加@RunWith(MockitoJUnitRunner.class)注解

  • 在测试类里添加方法,并让其在所有测试方法执行前执行

    // @Before注解表示在任意使用@Test注解标注的public void方法执行之前执行
    @Before
    public void init() {
        // 这句代码需要在运行测试函数之前被调用
        //MockitoAnnotations.openMocks(this);
        // 下面这条语句和上面的效果相同
        MockitoAnnotations.initMocks(this);
    }
    
  • 在测试类中添加

    @Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    

以上三种任选一种即可,建议用第一种。

下面给出完整的测试代码:

package cn.ecnu;

import java.util.List;

import cn.ecnu.domain.User;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.MockitoRule;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class MockTest {

    /*
    // @Before注解表示在任意使用@Test注解标注的public void方法执行之前执行
    @Before
    public void init() {
        // 这句代码需要在运行测试函数之前被调用
        //MockitoAnnotations.openMocks(this);
        // 下面这条语句和上面的效果相同
        MockitoAnnotations.initMocks(this);
    }*/

    /*@Rule
    public MockitoRule mockitoRule = MockitoJUnit.rule();*/

    // 只能作用于全局变量
    @Mock
    List mockedList;

    @Test
    public void mockTest() {
        // 测试桩,当指定的语句执行后返回对应的值或抛异常
        when(mockedList.get(0)).thenReturn("first");
        when(mockedList.get(1)).thenThrow(new RuntimeException());

        // 输出测试桩对应的内容first
        System.out.println(mockedList.get(0));
        // 输出测试桩对应的内容,抛异常
        System.out.println(mockedList.get(1));
    }
}

其输出和上面的代码一样

参数匹配器

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    
    // isA()的参数为一个类型,可以是任意类型。当调用的方法的参数为该类型时,该测试桩将会起效
    when(mockedList.get(isA(Integer.class))).thenReturn("isA");
    System.out.println(mockedList.get(0));
    // any()和isA()的作用完全一样
    when(mockedList.get(any(Integer.class))).thenReturn("any");
    System.out.println(mockedList.get(1));
    
    // 当参数为Int类型(Interger也算)时,返回对应的值
    when(mockedList.get(anyInt())).thenReturn("anyInt");
    // 当参数等于1时,返回对应的值
    when(mockedList.get(eq(1))).thenReturn("equal");

    // 输出测试桩对应的内容first
    System.out.println(mockedList.get(0));
    System.out.println("----------");
    // 输出测试桩对应的内容,抛异常
    System.out.println(mockedList.get(1));
}

以上代码的输出为:

isA
any
anyInt
----------
equal

而当将第15行注释后,输出为:

isA
any
anyInt
----------
anyInt

猜测是更准确的参数匹配器会覆盖不准确的,而不是同时起效。

当然自带的参数匹配器还有anyString()、anyList(),、anyCollection()等,不再赘述。

验证方法的调用次数

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    mockedList.add(1);
    mockedList.add(1);

    // 验证是否执行了恰好2次
    verify(mockedList,times(2)).add(1);
    // 验证是否执行了至少2次
    verify(mockedList,atLeast(2)).add(1);
    // 验证是否执行了至多2次
    verify(mockedList,atMost(2)).add(1);
    // 验证是否从未被执行
    verify(mockedList, never()).add(1);
    // 默认验证是否执行了恰好1次
    verify(mockedList).add(1);
}

如果验证通过,则不输出任何内容,反之输出对应的错误信息。

无返回值的方法执行时抛异常

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    // 无返回值的方法执行时抛异常
    doThrow(new RuntimeException()).when(mockedList).clear();
    mockedList.clear();
}

验证是否按指定顺序执行

验证单个mock对象的执行顺序

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    mockedList.add(1);
    mockedList.add(2);
    mockedList.add(3);

    // 为mock对象创建一个InOrder对象
    InOrder inOrder = inOrder(mockedList);
    // 验证是否按顺序执行
    inOrder.verify(mockedList).add(1);
    inOrder.verify(mockedList).add(2);
    inOrder.verify(mockedList).add(3);
}

当按指定顺序执行时不输出任何内容,反之输出错误信息。需要注意的是只要执行序列中出现了指定的这个序列就验证通过。比如,添加顺序为“1 3 2 3”或“1 2 3 4”都是验证通过的。

验证多个mock对象的执行顺序

@Test
public void mockTest() {
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);
    firstMock.add(1);
    secondMock.add(2);

    // 为mock对象创建一个InOrder对象
    InOrder inOrder = inOrder(firstMock,secondMock);
    // 验证是否按顺序执行
    inOrder.verify(firstMock).add(1);
    inOrder.verify(secondMock).add(2);
}

使用方法类似,不再赘述。

为连续的调用做测试桩

@Test
public void mockTest() {
    List mockedList = mock(List.class);
    when(mockedList.get(0))
        // 第一次调用时返回firstReturn
        .thenReturn("firstReturn")
        // 第二次调用时返回secondReturn
        .thenReturn("secondReturn")
        // 第三次(包括)之后的调用抛异常
        .thenThrow(new RuntimeException());
    mockedList.add(1);
    mockedList.add(2);
    // 第一次调用
    System.out.println(mockedList.get(0));
    // 第二次调用
    System.out.println(mockedList.get(0));
    // 非测试桩的调用
    System.out.println(mockedList.get(1));
    // 第三次调用
    System.out.println(mockedList.get(0));
}

上述代码的输出为:

firstReturn
secondReturn
null

java.lang.RuntimeException
......

一种更简洁的版本是把测试桩改成:

when(mockedList.get(0))
    .thenReturn("firstReturn", "secondReturn")
    .thenThrow(new RuntimeException());

其输出和之前一样。

调用原方法执行

class User{
    public int method(){
        return 222;
    }
}

@Test
public void mockTest(){
    User mockedUser = mock(User.class);
    when(mockedUser.method()).thenReturn(111);
    // 返回mock的结果111
    System.out.println(mockedUser.method());
    when(mockedUser.method()).thenCallRealMethod();
    // 调用原函数,返回222
    System.out.println(mockedUser.method());
}

监控真实对象

spy和mock是相反功能。
spy:如果不对spy对象的methodA打桩,那么调用spy对象的methodA时,会调用真实方法。
mock:如果不对mock对象的methodA打桩,将doNothing,且返回默认值(null,0,false)。

@Test
public void mockTest() {
    // 这里使用List.class会有问题
    List spyList = spy(ArrayList.class);
    when(spyList.size()).thenReturn(10086);
    // 效果和上个语句一样,类似的还有doThrow/doAnser等
    doReturn(10086).when(spyList).size();
    spyList.add(5);
    // 返回第一个元素5
    System.out.println(spyList.get(0));
    // 返回打桩的值10086
    System.out.println(spyList.size());
}

注解版:

@RunWith(MockitoJUnitRunner.class)
public class MockTest {

    @Spy
    List spyList = new ArrayList();
    // 如果是 List spyList; 则会有问题

    @Test
    public void mockTest() {
        when(spyList.size()).thenReturn(10086);
        // 效果和上个语句一样,类似的还有doThrow/doAnser等
        doReturn(10086).when(spyList).size();
        spyList.add(5);
        // 返回第一个元素5
        System.out.println(spyList.get(0));
        // 返回打桩的值10086
        System.out.println(spyList.size());
    }
}

超时验证

@RunWith(MockitoJUnitRunner.class)
public class MockTest {

    @Mock
    List mockedList;

    @Test
    public void mockTest() throws InterruptedException {
        mockedList.add(1);
        mockedList.add(1);
        // 验证在过去的100ms中是否执行了1次
        verify(mockedList,timeout(100)).add(1);
        // 验证在过去的100ms中是否执行了恰好2次
        verify(mockedList,timeout(100).times(2)).add(1);
        // 验证在过去的100ms中是否执行了至少2次
        verify(mockedList,timeout(100).atLeast(2)).add(1);
    }
}
;