mock和Mockito的关系 在软件开发中提及”mock”,通常理解为模拟对象。
为什么需要模拟? 在我们一开始学编程时,我们所写的对象通常都是独立的,并不依赖其他的类,也不会操作别的类。但实际上,软件中是充满依赖关系的,比如我们会基于service类写操作类,而service类又是基于数据访问类(DAO)的,依次下去,形成复杂的依赖关系。
单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。
有些时候,我们代码所需要的依赖可能尚未开发完成,甚至还不存在,那如何让我们的开发进行下去呢?使用mock可以让开发进行下去,mock技术的目的和作用就是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开 。
我们可以自己编写自定义的Mock对象实现mock技术,但是编写自定义的Mock对象需要额外的编码工作,同时也可能引入错误。现在实现mock技术的优秀开源框架有很多,Mockito就是一个优秀的用于单元测试的mock框架 。Mockito已经在github上开源,详细请点击:https://github.com/mockito/mockito
除了Mockito以外,还有一些类似的框架,比如:
EasyMock:早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常
PowerMock:这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了
JMockit:JMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode
Mockito已经被广泛应用,所以这里重点介绍Mockito。
Mockito使用举例 这里我们直接通过一个代码来说明mockito对单元测试的帮助,代码有三个类,分别如下: Person类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Person { private final int id; private final String name; public Person (int id, String name) { this .id = id; this .name = name; } public int getId () { return id; } public String getName () { return name; } }
PersonDAO:
1 2 3 4 5 6 public interface PersonDao { Person getPerson (int id) ; boolean update (Person person) ; }
PersonService:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class PersonService { private final PersonDao personDao; public PersonService (PersonDao personDao) { this .personDao = personDao; } public boolean update (int id, String name) { Person person = personDao.getPerson(id); if (person == null ) { return false ; } Person personUpdate = new Person(person.getId(), name); return personDao.update(personUpdate); } }
在这里,我们要进行测试的是PersonService类的update方法 ,我们发现,update方法依赖PersonDAO,在开发过程中,PersonDAO很可能尚未开发完成 ,所以我们测试PersonService的时候,所以该怎么测试update方法呢?连接口都还没实现,怎么知道返回的是true还是false?在这里,我们可以这样认为,单元测试的思路就是我们想在不涉及依赖关系的情况下测试代码。这种测试可以让你无视代码的依赖关系去测试代码的有效性。核心思想就是如果代码按设计正常工作,并且依赖关系也正常,那么他们应该会同时工作正常。所以我们的做法是mock一个PersonDAO对象,至于实际环境中,PersonDAO行为是否能按照预期执行,比如update是否能成功,查询是否返回正确的数据,就跟PersonService没关系了。PersonService的单元测试只测试自己的逻辑是否有问题
下面编写测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class PersonServiceTest { private PersonDao mockDao; private PersonService personService; @Before public void setUp () throws Exception { mockDao = mock(PersonDao.class); when(mockDao.getPerson(1 )).thenReturn(new Person(1 , "Person1" )); when(mockDao.update(isA(Person.class))).thenReturn(true ); personService = new PersonService(mockDao); } @Test public void testUpdate () throws Exception { boolean result = personService.update(1 , "new name" ); assertTrue("must true" , result); verify(mockDao, times(1 )).getPerson(eq(1 )); verify(mockDao, times(1 )).update(isA(Person.class)); } @Test public void testUpdateNotFind () throws Exception { boolean result = personService.update(2 , "new name" ); assertFalse("must true" , result); verify(mockDao, times(1 )).getPerson(eq(1 )); verify(mockDao, never()).update(isA(Person.class)); } }
我们对PersonDAO进行mock,并且设置stubbing,stubbing设置如下:
当getPerson方法传入1的时候,返回一个Person对象,否则默认返回空
当调update方法的时候,返回true
我们验证了两种情况:
更新id为1的Person的名字,预期:能在DAO中找到Person并更新成功
更新id为2的Person的名字,预期:不能在DAO中找到Person,更新失败
这样,根据PersonService的update方法的逻辑,通过这两个test case之后,我们认为代码是没有问题的。mockito在这里扮演了一个为我们模拟DAO对象,并且帮助我们验证行为(比如验证是否调用了getPerson方法及update方法)的角色
Android Studio工程配置Mockito Android Studio中使用Mockito非常简单,只需要在build.gradle文件中加入依赖即可。如图:
1 2 3 4 5 dependencies { ... testCompile 'org.mockito:mockito-core:1.10.19' ... }
Mockito使用方法 Mockito的使用,有详细的api文档,具体可以查看:http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html ,下面是整理的一些常用的使用方式。
验证行为
一旦创建,mock会记录所有交互,你可以验证所有你想要验证的东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testVerify () throws Exception { List mockedList = mock(List.class); mockedList.add("one" ); mockedList.add("two" ); mockedList.add("two" ); mockedList.clear(); verify(mockedList).add("one" ); verify(mockedList, times(2 )).add("two" ); verify(mockedList).clear(); }
Stubbing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void testStubbing () throws Exception { LinkedList mockedList = mock(LinkedList.class); when(mockedList.get(0 )).thenReturn("first" ); when(mockedList.get(1 )).thenThrow(new RuntimeException()); System.out.println(mockedList.get(0 )); System.out.println(mockedList.get(1 )); System.out.println(mockedList.get(999 )); verify(mockedList).get(0 ); }
对于stubbing,有以下几点需要注意:
对于有返回值的方法,mock会默认返回null、空集合、默认值。比如,为int/Integer返回0,为boolean/Boolean返回false
stubbing可以被覆盖,但是请注意覆盖已有的stubbing有可能不是很好
一旦stubbing,不管调用多少次,方法都会永远返回stubbing的值
当你对同一个方法进行多次stubbing,最后一次stubbing是最重要的
参数匹配 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testArgumentMatcher () throws Exception { LinkedList mockedList = mock(LinkedList.class); when(mockedList.get(anyInt())).thenReturn("element" ); System.out.println(mockedList.get(999 )); verify(mockedList).get(anyInt()); verify(mockedList).get(eq(33 )); }
如果你使用了参数匹配器,那么所有参数都应该使用参数匹配器
1 2 3 4 5 verify(mock).someMethod(anyInt(), anyString(), eq("third argument" )); verify(mock).someMethod(anyInt(), anyString(), "third argument" );
验证准确的调用次数,最多、最少、从未等 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Test public void testInvocationTimes () throws Exception { LinkedList mockedList = mock(LinkedList.class); mockedList.add("once" ); mockedList.add("twice" ); mockedList.add("twice" ); mockedList.add("three times" ); mockedList.add("three times" ); mockedList.add("three times" ); verify(mockedList).add("once" ); verify(mockedList, times(1 )).add("once" ); verify(mockedList, times(2 )).add("twice" ); verify(mockedList, times(3 )).add("three times" ); verify(mockedList, never()).add("never happened" ); verify(mockedList, atLeastOnce()).add("three times" ); verify(mockedList, atLeast(2 )).add("five times" ); verify(mockedList, atMost(5 )).add("three times" ); }
为void方法抛异常 1 2 3 4 5 6 7 8 @Test public void testVoidMethodsWithExceptions () throws Exception { LinkedList mockedList = mock(LinkedList.class); doThrow(new RuntimeException()).when(mockedList).clear(); mockedList.clear(); }
验证调用顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Test public void testVerificationInOrder () throws Exception { List singleMock = mock(List.class); singleMock.add("was added first" ); singleMock.add("was added second" ); InOrder inOrder = inOrder(singleMock); inOrder.verify(singleMock).add("was added first" ); inOrder.verify(singleMock).add("was added second" ); List firstMock = mock(List.class); List secondMock = mock(List.class); firstMock.add("was called first" ); secondMock.add("was called second" ); inOrder = inOrder(firstMock, secondMock); inOrder.verify(firstMock).add("was called first" ); inOrder.verify(secondMock).add("was called second" ); }
验证mock对象没有产生过交互 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testInteractionNeverHappened () { List mockOne = mock(List.class); List mockTwo = mock(List.class); verifyZeroInteractions(mockOne, mockTwo); mockOne.add("" ); verifyZeroInteractions(mockOne, mockTwo); }
查找是否有未验证的交互
不建议过多使用,api原文:A word of warning: Some users who did a lot of classic, expect-run-verify mocking tend to use verifyNoMoreInteractions() very often, even in every test method. verifyNoMoreInteractions() is not recommended to use in every test method. verifyNoMoreInteractions() is a handy assertion from the interaction testing toolkit. Use it only when it’s relevant. Abusing it leads to overspecified, less maintainable tests.
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testFindingRedundantInvocations () throws Exception { List mockedList = mock(List.class); mockedList.add("one" ); mockedList.add("two" ); verify(mockedList).add("one" ); verifyNoMoreInteractions(mockedList); }
@Mock注解
减少代码
增强可读性
让verify出错信息更易读,因为变量名可用来描述标记mock对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MockTest { @Mock List<String> mockedList; @Before public void initMocks () { MockitoAnnotations.initMocks(this ); } @Test public void testMock () throws Exception { mockedList.add("one" ); verify(mockedList).add("one" ); } }
根据调用顺序设置不同的stubbing 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private interface MockTest { String someMethod (String arg) ; } @Test public void testStubbingConsecutiveCalls () throws Exception { MockTest mock = mock(MockTest.class); when(mock.someMethod("some arg" )).thenThrow(new RuntimeException("" )).thenReturn("foo" ); mock.someMethod("some arg" ); System.out.println(mock.someMethod("some arg" )); System.out.println(mock.someMethod("some arg" )); when(mock.someMethod("some arg" )).thenReturn("one" , "two" , "three" ); }
doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()等用法 1 2 3 4 5 6 7 @Test public void testDoXXX () throws Exception { List mockedList = mock(List.class); doThrow(new RuntimeException()).when(mockedList).clear(); mockedList.clear(); }
spy监视真正的对象
spy是创建一个拷贝,如果你保留原始的list,并用它来进行操作,那么spy并不能检测到其交互
spy一个真正的对象+试图stub一个final方法,这样是会有问题的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Test public void testSpy () throws Exception { List list = new LinkedList(); List spy = spy(list); when(spy.size()).thenReturn(100 ); spy.add("one" ); spy.add("two" ); System.out.println(spy.get(0 )); System.out.println(spy.size()); verify(spy).add("one" ); verify(spy).add("two" ); when(spy.get(10 )).thenReturn("foo" ); doReturn("foo" ).when(spy).get(10 ); }
为未stub的方法设置默认返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testDefaultValue () throws Exception { List listOne = mock(List.class, Mockito.RETURNS_SMART_NULLS); List listTwo = mock(List.class, new Answer() { @Override public Object answer (InvocationOnMock invocation) throws Throwable { return null ; } }); }
参数捕捉 1 2 3 4 5 6 7 8 9 10 @Test public void testCapturingArguments () throws Exception { List mockedList = mock(List.class); ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class); mockedList.add("John" ); verify(mockedList).add(argument.capture()); assertEquals("John" , argument.getValue()); }
真正的部分模拟(TODO:尚未搞清楚啥意思。。。) 1 2 3 4 5 6 7 8 List list = spy(new LinkedList()); Foo mock = mock(Foo.class); when(mock.someMethod()).thenCallRealMethod();
重置mocks
Don’t harm yourself. reset() in the middle of the test method is a code smell (you’re probably testing too much).
1 2 3 4 5 6 7 8 @Test public void testReset () throws Exception { List mock = mock(List.class); when(mock.size()).thenReturn(10 ); mock.add(1 ); reset(mock); }
Serializable mocks
WARNING: This should be rarely used in unit testing.
1 2 3 4 @Test public void testSerializableMocks () throws Exception { List serializableMock = mock(List.class, withSettings().serializable()); }
更多的注解:@Captor, @Spy, @InjectMocks
@Captor 创建ArgumentCaptor
@Spy 可以代替spy(Object).
@InjectMocks 如果此注解声明的变量需要用到mock对象,mockito会自动注入mock或spy成员
1 2 3 4 5 6 7 @Spy BeerDrinker drinker = new BeerDrinker();@Spy BeerDrinker drinker;@InjectMocks LocalPub;
超时验证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private interface TimeMockTest { void someMethod () ; } @Test public void testTimeout () throws Exception { TimeMockTest mock = mock(TimeMockTest.class); verify(mock, timeout(100 )).someMethod(); verify(mock, timeout(100 ).times(1 )).someMethod(); verify(mock, timeout(100 ).times(2 )).someMethod(); verify(mock, timeout(100 ).atLeast(2 )).someMethod(); VerificationMode yourOwnVerificationMode = new VerificationMode() { @Override public void verify (VerificationData data) { } }; verify(mock, new Timeout(100 , yourOwnVerificationMode)).someMethod(); }
查看是否mock或者spy 1 2 Mockito.mockingDetails(someObject).isMock(); Mockito.mockingDetails(someObject).isSpy();