Mockito使用总结

Mockito使用总结

写UT时,经常会遇到执行过程中调用的方法返回结果不可控的情况,为了专注在某个确定范围内的开发自测,需要模拟这些方法和类的行为,Mockito提供了很好的解决方案。

使用Mockito可以很方便的设置、校验方法或类的行为,但是前提是首先创建一个mock对象,才能基于Mockito进行操作。

创建一个mock对象

可以简单的调用mock方法来创建一个mock对象:

List mockedList = Mockito.mock(List.class)

或者更简单地,可以直接用@mock:

@Mock
List mockedList;

如果要使用注解,必须打开注解开关:

MockitoAnnotations.initMocks(this);

设置mock对象的行为

我们可以设置mock对象调用某个方法的返回值:

Mockito.when(mockedList.get(0)).thenReturn("val");

或者设置调用方法时抛出某个异常:

Mockito.when(mockedList.get(1)).thenThrow(new RuntimeException());

对于难以mock的方法,直接跳过它:

Mockito.doNothing().when(testObject).handle();

对于一个mock对象,没有设置过的方法行为均返回null:

mockedList.get(999) // 将返回null

在实际使用中常常设置某个方法的返回值为另一个mock对象,在复杂的情况时可以以此来控制整个测试过程。

验证mock对象的行为

1、验证mock对象是否调用过某个方法:

Mockito.verify(mockedList).add("one");  // 是否执行过mockedList.add("one")

注意验证的目标对象必须是mock的,否则会报错。

2、验证方法调用了多少次、是否从未调用过:

Mockito.verify(mockedList, Mockito.times(2)).add("one");  // 是否调用过两次add("one")
Mockito.verify(mockedList, Mockito.never()).add("one");  // 是否从未调用过add("one")

3、验证某个mock对象是否从未使用过:

Mockito.verifyZeroInteractions(mockedList);

4、验证同一个mock对象执行不同方法的先后顺序:

List singleMock = Mockito.mock(List.class);
singleMock.add("first");
singleMock.add("second");
InOrder inOrder = Mockito.inOrder(singleMock);
// 验证调用顺序
inOrder.verify(singleMock).add("first");
inOrder.verify(singleMock).add("second");

5、验证不同mock对象执行不同方法的先后顺序:

List firstMock = Mockito.mock(List.class);
List secondMock = Mockito.mock(List.class);
firstMock.add("first");
secondMock.add("second");
InOrder inOrder = Mockito.inOrder(firstMock,secondMock);
// 验证调用顺序
inOrder.verify(firstMock).add("first");
inOrder.verify(secondMock).add("second");

匹配器

匹配器可以代替一类参数:

1、验证一类行为:

// 验证是否执行过mockedList的get方法,参数为任意int数
Mockito.verify(mockedList).get(Mock.anyInt()); 

2、设置一类行为:

// 当mockedList调用get方法,参数为任意int数,返回值都是element
Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("element");

参数捕获验证

参数捕获提供了一种验证的方式:

IUserService userService = Mockito.mock(UserServiceImpl.class);
ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
//参数捕获
Mockito.verify(userService).addUser(argument.capture());
//验证
Assert.assertEquals(userName, argument.getValue());

使用Answer接口mock

有时要设置一个对象的复杂行为,这些复杂行为要根据入参的实际情况来区分不同逻辑,此时就需要用Answer接口,如设置除数为0抛异常:

Division testObject = Mockito.mock(Division.class);

//如果是无返回值得 方法可以去掉泛型。把返回值改为Object
Mockito.doAnswer(newAnswer<Integer>() {

    @Override
    public Integer answer(InvocationOnMock invocation) throws Throwable {

        Object[] arguments = invocation.getArguments();
        int b = (int) arguments[1];
        if(b == 0){
            throw new RuntimeException("除数为零");
        }
        //也可以返回任意需要的值
        return (Integer) invocation.callRealMethod();
    }
}).when(testObject).divide(Mockito.anyInt(), Mockito.anyInt());
testObject.divide(10, 0);

可以使用doAnswer来mock,也可以直接将Answer对象放入then方法的入参:

Division testObject = Mockito.mock(Division.class);
Mockito.when(testObject.divide(Mockito.anyInt(), Mockito.anyInt())).then(newAnswer<Integer>() {

    @Override
    public Integer answer(InvocationOnMock invocation) throws Throwable {

        Object[] arguments = invocation.getArguments();
        int b = (int) arguments[1];
        if(b == 0){
            throw new RuntimeException("除数为零");
        }
        //可以返回任意值
        return (Integer) invocation.callRealMethod();
    }
});
testObject.divide(10, 0);

部分模拟

1、部分模拟的概念

前面提到,如果某个对象被mock,那么它将不再拥有原本的功能,全部的方法都被替换掉了,只会返回设置好的值,如果mock对象的某个方法没有设置,那么就会返回null。当我们想要验证某个对象的行为,同时又需要它一部分功能保持不变时,就要使用部分模拟:

// 使用部分模拟需要用spy方法,它必须是某个已经建立好的对象的封装,不能用class对象为构造参数
List list = new LinkedList();
List mockedList = Mockito.spy(list);

后续我们可以正常调用它的方法,模拟它的部分行为,并验证它的功能:

// 设置method1方法的行为
Mockito.when(mockedList.method1(Mockito.anyInt())).thenReturn("element");
// 上述方式可能报错,也可以这样写
Mockito.doReturn("element").when(mockedList).method1(Mockito.anyInt());

// 调用method2将正常返回原本该返回的值,走正常的方法流程,与mock无关
mockedList.method2();

// 验证对象的行为
Mockito.verify(mockedList).method1(Mock.anyInt()); 

2、不可模拟情况的替代方案

一旦对象是部分模拟的,在设置行为的过程中就可能发生异常,比如对于一个空集合mockedList,当执行下列语句时就会直接抛出越界异常:

Mockito.when(mockedList.get(0)).thenReturn("foo");

这种情况下应该用这种形式来模拟行为:

Mockito.doReturn("foo").when(mockedList).get(0);

3、使用mock来部分模拟

部分模拟也可以不用spy方法,使用mock一样可以完成相同的功能:

List mockedList = Mockito.mock(List.class);

// 调用某个方法时返回真实值
Mockito.when(mockedList.someMethod()).thenCallRealMethod();

4、注解的使用

和mock一样,spy方法也可以方便的用注解来代替,此时也要注意必须使用初始化后的对象,同时开启注解:

@Spy
List list = new LinkedList();

5、使用部分模拟来完成对局部变量的替换

假设我们要测试这样一段代码:

Request request = new Request();

// 设置request的属性
...

// 调用方法
Response response = serviceInvoker.invoke(SERVICE_NAME, request);

如果我们想模拟invoke方法的行为,很难准确的确定request这个参数,即使创建一个相同的对象,设置其行为:

// 创建request对象并设置其属性
Mockito.when(serviceInvoker.invoke(SERVICE_NAME, request)).thenReturn("foo");

这样也是行不通的,因为在待测试的代码中,request是一个局部变量,它的地址和我们创建出来的对象地址是不同的,也就无法准确的进行模拟。

这种情况解决方案是在待测试代码中将创建request的方法抽取出来:

Request request = getRequest();

// 调用方法
Response response = serviceInvoker.invoke(SERVICE_NAME, request);

然后在UT中,首先mock这个方法getRequest,使其返回值是我们控制好的request对象,然后再进行测试即可,这样就能将我们预期的request对象放入方法中了:

// 创建request对象并设置其属性
Request request = new Request();
...

// 模拟getRequest方法的行为
Mockito.when(instance.getRequest()).thenReturn(request);

// 完成我们一开始想要的行为定义
Mockito.when(serviceInvoker.invoke(SERVICE_NAME, request)).thenReturn("foo");

@InjectMocks

日常开发过程中,另外一个常用的注解是@InjectMocks,用它标注的类会自动装配其中已经被@Mock和@Spy标注的字段:

// ModelDaoImpl类中有一个字段mediator, 这里自动装配其中的mediator字段
@InjectMocks
ModelDaoImpl modelDao; 

@Mock
Mediator mediator;

...{
    // 这里调用的create是真实的方法,这里面如果有mediator调用某个方法,就可以通过自动装配事先设置它的行为了
    Mockito.when(mediator.get(0)).thenReturn("foo");
    modelDao.create();
    ...
}

PowerMock

mock静态方法

PowerMock可以来配合Mockito使用,模拟静态方法的行为。Mockito不能来mock final的类,此时也需要借助PowerMock。

1、使用前的准备

需要在测试类上加注解:

@RunWith(PowerMockRunner.class) // 一个固定的启动类
@PrepareForTest({Factory.class}) // 设置要模拟的静态方法对应的类

2、设置静态方法、或者新建对象的行为:

PowerMockito.mockStatic(Factory.class); // 用spyStatic可以部分模拟,在每次设置行为前都要执行该方法
PowerMockito.when(Factory.getLogger()).thenReturn(logger); // 设置Factory.getLogger()方法返回值

PowerMockito.doThrow(new RunTimeException()).when(Factory.class) // 执行返回值为void的方法时抛出异常

PowerMockito.whenNew(MyClass.class).withNoArguments().thenThrow(new IOEeception()); // 新建对象时抛出异常

3、验证静态方法、或者新建对象的行为:

// 检查getLogger是否调用了2次
PowerMockito.verifyStatic(Mockito.times(2));
Factory.getLogger();

// 检查getLogger是否调用了1次
PowerMockito.verifyStatic(); // default times is once
Factory.getLogger();

// 检查getLogger是否从未被调用
PowerMockito.verifyStatic(Mockito.never());
Factory.getLogger();

// 是否新建过MyClass类的对象
PowerMockito.verifyNew(MyClass.class).withNoArguments();

4、私有方法的模拟与验证:

// classUnderTest调用私有方法methodName,参数为parameter时,返回值为value
PowerMockito.doReturn(value).when(classUnderTest, "methodName", "parameter");

// classUnderTest是否调用两次私有方法methodName,参数为parameter
PowerMockito.verifyPrivate(classUnderTest, Mockito.times(2)).invoke("methodName", "parameter");

5、mock构造方法

运行测试代码时,有时不希望运行构造方法,此时需要mock 构造方法。mock的类需要加入@PrepareForTest:

Bucket bucket = Mockito.mock(Bucket.class);
PowerMockito.whenNew(Bucket.class).withNoArguments().thenReturn(bucket);

用Whitebox代替反射

1、设置私有成员变量

有时在UT中需要设置某个类私有成员变量的值,需要用反射来处理,非常繁琐。powermock给我们提供了简单的API,可以方便的设置私有成员变量的值。

如对象recevier中有私有成员变量名为trainModelDao,同时有另一个对象为trainModelDao,将该对象之间设置到recevier的私有成员变量中:

Whitebox.setInternalState(recevier, "trainModelDao", trainModelDao);

2、直接跳过构造方法创建一个对象:

ExampleWithEvilConstructor tested = Whitebox.newInstance(ExampleWithEvilConstructor.class);
assertNull(tested.getMessage());

几个易错点需要注意

1、有几个@Test标签,就会执行几次@Before @Test @After ,而@BeforeClass和@AfterClass只在一个测试类中执行一次

2、有多个@Test标签时,测试类的构造函数、非static修饰的字段会初始化多次

posted @ 2020-12-28 21:38  勇闯8  阅读(2946)  评论(0编辑  收藏  举报