为什么要mock?
有很多朋友不愿意写单元测试,觉得写单测试比较花时间,甚至不会写单元测试,很大程度上是因为不想写或者不会写mock。
mock对于单元测试来很重要。单元测试之所以名字里面有“单元”,就是因为一个测试用例只测很小的一个单元代码。但我们的代码总会有依赖,要测试的方法内部可能调用了其它类的方法,这在代码中是很常见的逻辑。所以我们经常会需要mock,用来消除依赖。
所谓mock,翻译过来就是“模拟”,就是模拟你要测试的代码里面「依赖的其它对象」,模拟它的输入和输出,这样就不用管其它逻辑是如何实现的,我们「假定它是符合我们期望的就行了」。
可能你会有一个疑问:那我们在单元测试里面假定它符合期望了,但实际运行时它有bug了,并不是符合我们期望的,怎么办呢?
首先,我们对那个类也会做单元测试,我们用它的单元测试保证了它的逻辑是正确的。
然后,一个完整的测试体系,不应该只有单元测试。单元测试之上,还应该有集成测试、API测试等,从更高的层面来保证整个应用程序能够如预期工作。
Mockito的基本用法
Mockito是Java语言非常主流的一个框架,自己使用起来感觉也比较好用。所以这篇文章想汇总介绍一下Mockito的各种用法,这样大家在以后写单元测试和看单元测试的时候,就能够比较清晰为什么要这么写。
设置Mockito环境
要使用Mockito,首先得在测试类里面设置好Mockito环境。这是为了能够让单元测试框架(本文主要介绍JUnit)能够识别和使用Mockito。
在JUnit 4, JUnit使用了@RunWith
注解来声明一个“运行器”。这个运行期的作用是为单元测试提供「mock的初始化工作」(比如使用@Mock、@Spy等注解时,需要初始化),以及「验证mock语法」的功能。
比如我们可能会经常用到的:
@RunWith(JUnit4.class) @RunWith(SpringRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
Mockito也有相应的启动器,在@RunWith注解上面使用这个启动器就可以使用Mockito的环境了:
@RunWith(MockitoJUnitRunner)
在JUnit 5,使用了@ExtendWith
注解来代替@RunWith注解,Mockito也支持JUnit 5,提供了MockitoExtension
类。
除了使用注解以外,也可以使用静态方法initMocks来实现这个功能:
MockitoAnnotations.initMocks(this)
mock
mock,即mock一个对象。也是也注解和代码两种方式可以实现。
@Mock private User user; Order order = Mockito.mock(Order.class);
mock对象后,就可以对它使用given等方法模拟它的输入和输出。
given(order.getId()).willReturn(1L); assertEquals(order.getId(), 1L);
spy
mock出来的对象是完全虚拟的,不会真正地调用本来的实现。如果不对它使用given等方法,会返回默认值(null, 0, false等)。而spy如果不使用given等方法,会调用这个对象本来的实现,返回实际运行后的值。
❝不是很推荐使用spy,因为它没有消除依赖
❞
spy同样有注解和静态方法的方式:
@Spy private user user; Order order = Mockito.spy(Order.class);
captor
captor翻译过来是“捕获”的意思,主要用来捕捉程序运行时调用mock或者spy的对象的方法时,传入的参数。它支持泛型,即要捕获的参数的类型。
@Captor ArgumentCaptor<User> userCaptor; ArgumentCaptor<String> arg = ArgumentCaptor.forClass(String.class);
captor一般是与given或者verify等方法配合使用。
@Mock List mockedList; @Captor ArgumentCaptor argCaptor; @Test public void whenUseCaptorAnnotation_thenTheSam() { mockedList.add("one"); Mockito.verify(mockedList).add(argCaptor.capture()); assertEquals("one", argCaptor.getValue()); }
InjectMocks
使用@InjectMocks
注解,可以将mock或spy的对象自动注入要测试的对象。这在Spring等使用自动注入的框架里用得非常广泛。
@Mock Map<String, String> wordMap; @InjectMocks MyDictionary dic = new MyDictionary(); // 类定义: class MyDictionary { Map<String, String> wordMap; public MyDictionary() { wordMap = new HashMap<String, String>(); } public void add(final String word, final String meaning) { wordMap.put(word, meaning); } public String getMeaning(final String word) { return wordMap.get(word); } }
打桩
所谓打桩,其实就是mock的一个过程。我们给定期望的输入和输入,mock的对象就能够如我们期望的那样工作。
打桩有两种写法,一种是传统的写法,一种是BDD **Behavior-Driven Development (行为驱动开发)**的写法。
传统写法
我们先看看传统的写法,传统的写法主要用Mockito
类的方法。基本是when-then-invoke-verify形式。
when(phoneBookRepository.contains(momContactName)) .thenReturn(false); phoneBookService.register(momContactName, momPhoneNumber); verify(phoneBookRepository) .insert(momContactName, momPhoneNumber);
BDD写法
BDD写法主要是用BDDMockito
类的方法,看起来是given–will-invoke-then的形式。
given(phoneBookRepository.contains(momContactName)) .willReturn(false); phoneBookService.register(momContactName, momPhoneNumber); then(phoneBookRepository) .should() .insert(momContactName, momPhoneNumber);
动态mock
有时候我们可能会遇到这个问题:在方法内部可能有循环或者递归,会多次调用其它对象的方法,但输入的参数不同,我们在mock的时候,期望它根据不同的输入参数返回不同的结果。这个时候如果一个一个写given,就需要写很多次。但其实可以用动态mock来做。它是基于InvocationOnMock
来实现的。
given(phoneBookRepository.contains(momContactName)) .willReturn(true); given(phoneBookRepository.getPhoneNumberByContactName(momContactName)) .will((InvocationOnMock invocation) -> invocation.getArgument(0).equals(momContactName) ? momPhoneNumber : null); phoneBookService.search(momContactName); then(phoneBookRepository) .should() .getPhoneNumberByContactName(momContactName);
Mockito的不足
Mockito可以说是Java语言最流行的mock框架了。但它不是所有对象都可以mock的,有一些限制。
Mockito在3.4.0以前,是不能mock静态方法的。这取决于它的底层实现,是使用动态代理来做的。而动态代理是代理静态方法、final方法和private方法的。
一般来说,我们不应该mock静态方法、final方法和private方法的。但有时候可能会有这样的需求,比如apache和guava框架,就使用了大量的静态方法提供一些工具类。如果需要Mock的话,就得配合PowerMock等框架来实现。但PowerMock框架目前还不支持JUnit 5。
在Mockito 3.4.0,开始支持mock静态方法,底层是通过修改字节码来实现的。但性能很差,mock一个实例大概要一秒多,大家酌情使用。