在单元测试过程中有时候Mock框架是必不可少的,通过Mock框架可以用来模拟对象的行为。这里我们以目前主流的Mockito框架进行介绍
POM依赖
这里我们向POM中引入Mockito依赖,同时这里对于Junit框架,我们选用Junit 5版本
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.5.2</version> </dependency>
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>3.1.0</version> </dependency>
|
初探
引入依赖后我们来看看如何使用该框架来Mock对象,并对相关方法进行打桩。不难看出,其基本用法还是很简单的。如果Mock对象的方法被打桩了,则调用时会返回指定值;反之则会按照方法声明的返回值类型返回相应的空值。换言之,对于Mock而言,Mock的目标既可以是类、也可以是接口
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 MockDemo1 {
@Test public void basic() { List<String> list = Mockito.mock( List.class );
Mockito.when( list.get(0) ) .thenReturn( "Hello" ); Assertions.assertEquals("Hello", list.get(0));
Mockito.when( list.get(3) ) .thenReturn( "Aaron" ); Assertions.assertEquals("Aaron", list.get(3));
Mockito.when( list.remove("Tony") ) .thenThrow( new RuntimeException("不能没有Tony老师") );
try { list.remove("Tony"); Assertions.fail(); } catch (Exception e) { Assertions.assertTrue( e instanceof RuntimeException ); Assertions.assertEquals("不能没有Tony老师", e.getMessage()); }
Assertions.assertEquals(null, list.get(996)); Assertions.assertEquals(0, list.size()); Assertions.assertEquals( false, list.contains( "China" ) ); } }
|
参数匹配
在对方法进行打桩的过程中,我们还可以进行模糊匹配参数值。故在ArgumentMatchers类中内置了很多常见的参数匹配器。当然如果内置的参数匹配器无法满足时,我们还可以通过实现ArgumentMatcher函数式接口来自定义参数匹配器。然后通过argThat方法传入自定义参数匹配器即可
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 36 37 38
| public class MockDemo1 {
@Test public void testArgumentMatcher() { LinkedList<String> list1 = Mockito.mock( LinkedList.class );
Mockito.when( list1.addAll( ArgumentMatchers.any() ) ) .thenReturn( true ); Assertions.assertTrue( list1.addAll(null) ); Assertions.assertTrue( list1.addAll(new HashSet()) );
Mockito.when( list1.addAll(ArgumentMatchers.anyInt(), ArgumentMatchers.anyList() ) ) .thenReturn( true ); Assertions.assertTrue( list1.addAll(24, new ArrayList()) ); Assertions.assertFalse( list1.addAll(24, new TreeSet()) );
Mockito.when( list1.add( ArgumentMatchers.anyString() )) .thenReturn(true); Assertions.assertTrue( list1.add("Bob") );
Mockito.when( list1.remove( ArgumentMatchers.anyInt() ) ) .thenReturn("删除指定位置上的元素成功"); Assertions.assertEquals( "删除指定位置上的元素成功", list1.remove(25) );
ArgumentMatcher<String> argumentMatcher = e -> e != null && e.length()<5; Mockito.when( list1.contains( ArgumentMatchers.argThat(argumentMatcher) ) ) .thenReturn(true); Assertions.assertTrue( list1.contains("Tony") ); } }
|
then 系列方法
thenReturn 方法
通过thenReturn可以实现对方法进行打桩以返回指定值
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 36 37 38 39 40 41 42 43
| public class MockDemo1 {
public void testThenReturn() { List<Integer> list = Mockito.mock( List.class );
Mockito.when( list.get(0) ) .thenReturn(27); Mockito.when( list.get(0) ) .thenReturn(11); Mockito.when( list.get(0) ) .thenReturn(24); Assertions.assertEquals(24, list.get(0));
Mockito.when( list.get(1) ) .thenReturn(238) .thenReturn(179) .thenReturn(996); Assertions.assertEquals( 238, list.get(1) ); Assertions.assertEquals( 179, list.get(1)); Assertions.assertEquals( 996, list.get(1)); Assertions.assertEquals( 996, list.get(1) );
Mockito.when( list.get(2) ) .thenReturn(137, 12, 139); Assertions.assertEquals( 137, list.get(2) ); Assertions.assertEquals( 12, list.get(2) ); Assertions.assertEquals( 139, list.get(2) ); Assertions.assertEquals( 139, list.get(2) ); } }
|
thenThrow 方法
通过thenThrow可以实现对方法进行打桩,以实现对方法调用抛出指定异常
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
| public class MockDemo1 {
@Test public void testThenThrow() { List<Integer> list = Mockito.mock( List.class );
Mockito.when( list.get(1) ) .thenThrow( new RuntimeException("非法操作") ) .thenReturn( 12 ) .thenThrow( new RuntimeException("目标类不存在") ) .thenReturn(23);
Exception ex1 = Assertions.assertThrows(RuntimeException.class, ()->list.get(1) ); Assertions.assertEquals("非法操作", ex1.getMessage() );
Assertions.assertEquals( 12, list.get(1) );
Exception ex3 = Assertions.assertThrows(RuntimeException.class, ()->list.get(1) ); Assertions.assertEquals("目标类不存在", ex3.getMessage() );
Assertions.assertEquals( 23, list.get(1) );
Assertions.assertEquals( 23, list.get(1) ); } }
|
then、thenAnswer 方法
通过then可以实现对方法进行打桩,以实现自定义返回值逻辑。具体地我们只需实现函数式接口Answer,并实现自定义返回值逻辑即可。特别地,then 方法 和 thenAnswer 方法作用是相同的
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
| public class MockDemo1 {
public void testThen() { Map<Integer, Integer> map = Mockito.mock( HashMap.class );
Mockito.when( map.put(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) ) .then( invocation -> { Object[] args = invocation.getArguments(); Integer key = (Integer) args[0]; Integer value = (Integer)args[1]; if( key.equals(1) ) { return 110; } else if( key.equals(2) ) { return 120; } else if( key < 100 ) { return key+value; } else { return value; } } );
Assertions.assertEquals(110, map.put(1, 37)); Assertions.assertEquals(120, map.put(2, 37)); Assertions.assertEquals(10086, map.put(86, 10000)); Assertions.assertEquals(520, map.put(996, 520)); } }
|
do 系列方法
doReturn 方法
与thenReturn方法作用类似,doReturn 方法也可以实现对方法进行打桩以返回指定值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class MockDemo1 {
@Test public void testDoReturn() { List<Integer> list1 = Mockito.mock( List.class ); Mockito.doReturn(14) .doReturn(27) .when(list1) .get(996);
Assertions.assertEquals( 14, list1.get(996) ); Assertions.assertEquals( 27, list1.get(996) ); Assertions.assertEquals( 27, list1.get(996) ); } }
|
doThrow 方法
与thenThrow方法作用类似,doThrow 方法也可以实现对方法进行打桩,以实现对方法调用抛出指定异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class MockDemo1 {
@Test public void testDoThrow() { List<Integer> list1 = Mockito.mock( List.class ); Mockito.doThrow(new RuntimeException("今晚的月色真美")) .doThrow( new RuntimeException("风也温柔") ) .when(list1) .get(996);
Exception ex1 = Assertions.assertThrows( RuntimeException.class, ()->list1.get(996) ); Assertions.assertEquals("今晚的月色真美", ex1.getMessage() );
Exception ex2 = Assertions.assertThrows( RuntimeException.class, ()->list1.get(996) ); Assertions.assertEquals("风也温柔", ex2.getMessage() );
Exception ex3 = Assertions.assertThrows( RuntimeException.class, ()->list1.get(996) ); Assertions.assertEquals("风也温柔", ex3.getMessage() ); }
}
|
doAnswer 方法
与then、thenAnswer方法作用类似,doAnswer 方法也可以实现对方法进行打桩。以实现自定义返回值逻辑。具体地我们只需实现函数式接口Answer,并实现自定义返回值逻辑即可
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
| public class MockDemo1 {
@Test public void testDoAnswer() { List<String> list1 = Mockito.mock( List.class );
Mockito.doAnswer( invocation -> { Object[] args = invocation.getArguments(); Integer index = (Integer) args[0]; if ( index > 77 ) { return "Aaron"; } else if (index < 77){ return "Tony"; } else { return "Tim"; } }) .when( list1 ) .get( ArgumentMatchers.anyInt() );
Assertions.assertEquals("Aaron", list1.get(97) ); Assertions.assertEquals("Tony", list1.get(13) ); Assertions.assertEquals("Tim", list1.get(77)); }
}
|
doNothing 方法
doNothing 方法可以对Void Method空方法进行打桩,其中所谓Void Method空方法指的是返回类型为void的方法。显然,对于Mock对象的Void Method空方法而言,没有打桩的必要。因为不打桩也不会调用真实的方法逻辑。故其更多的应用场景是对Spy对象的Void Method空方法进行打桩,使得当调用Void Method时,不会再调用真实的方法
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
| public class MockDemo1 {
@Test public void testDoNothing() { Person person2 = Mockito.spy(Person.class); person2.hello("Bob"); Mockito.doNothing() .when( person2 ) .hello( "奥特曼" ); person2.hello("奥特曼"); }
}
class Person { ...
public void hello(String name) { System.out.println("Hello, I'm "+name); }
... }
|
测试结果如下,可以看到Void Method空方法一旦被打桩掉之后。原方法就不会再执行了,控制台也不会输出了
reset 方法
reset 方法可以重置Mock、Spy的对象,移除对其的所有打桩
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class MockDemo1 {
@Test public void testReset() { Person person1 = Mockito.mock(Person.class); Mockito.doReturn(200) .when( person1 ) .calc(13, 31); Mockito.doReturn(500) .when( person1 ) .calc(3, 10); Assertions.assertEquals(200, person1.calc(13,31)); Assertions.assertEquals(500, person1.calc(3,10)); Mockito.reset( person1 ); Assertions.assertEquals(0, person1.calc(13,31)); Assertions.assertEquals(0, person1.calc(3,10));
Person person2 = Mockito.spy(Person.class); Mockito.doReturn(200) .when( person2 ) .calc(13, 31); Mockito.doReturn(500) .when( person2 ) .calc(3, 10); Assertions.assertEquals(200, person2.calc(13,31)); Assertions.assertEquals(500, person2.calc(3,10)); Mockito.reset( person2 ); Assertions.assertEquals(44, person2.calc(13,31)); Assertions.assertEquals(13, person2.calc(3,10)); }
}
class Person { ...
public int calc(Integer a, Integer b) { return a+b; }
... }
|
Spy对象
与Mock对象是模拟的不同,Spy出来的对象是一个真实的对象。所以对于Spy出来的对象而言,如果调用未打桩的方法, 其会调用真实的方法。并根据调用结果返回真实的返回值;反之,如果Spy对象的方法被打桩了,则后续调用该方法时,真实方法将不会再被被调用,并返回打桩后值。这里特别说明下,在对Spy对象进行打桩过程中。如果使用then系列方法进行打桩, 则会导致真实方法在打桩过程中被调用;而如果使用do系列方法进行打桩, 则真实方法在打桩过程中不会被调用
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| public class SpyDemo1 {
@Test public void testDiffMockSpy() { Animal animal1 = Mockito.mock( Animal.class );
Assertions.assertNull( animal1.getInfo("Aaron") );
Mockito.when( animal1.getInfo("Bob") ) .thenReturn("Hi, Bob"); Assertions.assertEquals("Hi, Bob", animal1.getInfo("Bob"));
Mockito.doReturn( "Hi, Tony" ) .when( animal1 ) .getInfo("Tony"); Assertions.assertEquals("Hi, Tony", animal1.getInfo("Tony"));
Animal animal2 = Mockito.spy( Animal.class );
Assertions.assertEquals("I'm 张三", animal2.getInfo("张三") );
Mockito.when( animal2.getInfo(ArgumentMatchers.anyString() ) ) .thenReturn("我是李四"); Assertions.assertEquals("我是李四", animal2.getInfo("李四"));
Mockito.doReturn("我是王二麻子") .when( animal2 ) .getInfo("王二麻子"); Assertions.assertEquals("我是王二麻子", animal2.getInfo("王二麻子")); } }
class Animal { public String getInfo(String name) { String info = "I'm " + name; System.out.println( info ); return info; } }
|
then系列方法、do系列方法 区别
虽然then系列方法、do系列方法的作用上是相同的。但在某些场景时可能无法使用then系列方法,使得我们只能使用do系列方法
对Spy对象进行打桩
前面提到,在对Spy对象进行打桩过程中。如果使用then系列方法进行打桩, 则会导致真实方法在打桩过程中被调用。此时即可能会导致打桩失败。这时则应该使用do系列方法进行打桩
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| public class DoThenDiff {
@Test public void stubOnSpy() { LinkedList<Integer> list1 = Mockito.spy( LinkedList.class );
try{ Mockito.when( list1.get(3) ) .thenReturn(32); } catch (IndexOutOfBoundsException e) { System.out.println("[thenReturn] Happen Exception : " + e.getMessage()); }
Mockito.doReturn(132) .when( list1 ) .get( 13 ); Assertions.assertEquals(132, list1.get(13));
try{ Mockito.when( list1.get(4) ) .thenThrow( new RuntimeException("非法操作") ); } catch (IndexOutOfBoundsException e) { System.out.println("[thenThrow] Happen Exception : " + e.getMessage()); }
Mockito.doThrow( new RuntimeException("这不是非法操作") ) .when( list1 ) .get( 14 ); Exception ex1 = Assertions.assertThrows(RuntimeException.class, ()->list1.get(14) ); Assertions.assertEquals("这不是非法操作", ex1.getMessage() );
try{ Mockito.when( list1.get(5) ) .then( invocation -> 211 ); } catch (IndexOutOfBoundsException e) { System.out.println("[then] Happen Exception : " + e.getMessage()); }
Mockito.doAnswer( invocationOnMock -> 985 ) .when( list1 ) .get(15); Assertions.assertEquals(985, list1.get(15) ); } }
|
测试结果如下所示
对Void Method进行打桩
then系列方法无法对Void Method方法进行打桩,其会导致无法通过编译。故此时也只能选择do系列方法进行打桩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class DoThenDiff {
@Test public void stubOnVoidMethod() { List<String> list1 = Mockito.mock(LinkedList.class);
Mockito.doThrow( new RuntimeException("风也温柔") ) .when( list1 ) .add(3, "Aaron"); Exception ex1 = Assertions.assertThrows(RuntimeException.class, ()->list1.add(3, "Aaron") ); Assertions.assertEquals("风也温柔", ex1.getMessage() ); }
}
|
基于注解的模拟
前面我们通过都是Mockito的mock、spy方法来模拟对象。事实上,我们还可以通过@Mock、@Spy注解标识其是一个Mock、Spy的对象。为了让这两个注解生效、并生成相应的对象。我们有两种方式实现
- 方式1:在测试类上添加 @ExtendWith(MockitoExtension.class) 注解
- 方式2:在测试类的初始化方法添加 MockitoAnnotations.initMocks(this) 实现
进一步地,我们还可以使用 @InjectMocks 注解以实现将@Mock、@Spy修饰的对象自动注入到@InjectMocks修饰的对象中。其支持字段注入、构造器注入、set方法注入
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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| @ExtendWith(MockitoExtension.class) public class AnnotationDemo { @Mock private Man man;
@Spy private Woman woman;
@InjectMocks @Spy private Human human;
@BeforeEach public void init() { }
@Test public void test1() { Assertions.assertNull( man.hello("Aaron") ); Mockito.doReturn( "你好, 我是男人" ) .when( man ) .hello( ArgumentMatchers.anyString() ); Assertions.assertEquals("你好, 我是男人", man.hello("Aaron") );
Assertions.assertEquals("Bye, I'm Tony", woman.bye("Tony") ); Mockito.doReturn( "你好, 我是女人" ) .when( woman ) .bye( ArgumentMatchers.anyString() ); Assertions.assertEquals("你好, 我是女人", woman.bye("Tony") ); }
@Test public void test2() { String info1 = human.getInfo("张三"); Assertions.assertEquals("null...Bye, I'm 张三", info1);
Mockito.doReturn( "你好, 我是男人" ) .when( man ) .hello( ArgumentMatchers.anyString() ); Mockito.doReturn( "你好, 我是女人" ) .when( woman ) .bye( ArgumentMatchers.anyString() );
String info2 = human.getInfo("李四"); Assertions.assertEquals("你好, 我是男人...你好, 我是女人", info2); }
}
class Human { private Man man;
private Woman woman;
public String getInfo(String name) { String helloMsg = man.hello(name); String byeMsg = woman.bye(name); String info = helloMsg + "..." + byeMsg; return info; } }
class Man { public String hello(String name) { String info = "Hello, I'm " + name; return info; } }
class Woman { public String bye(String name) { String info = "Bye, I'm " + name; return info; } }
|