0%

单元测试之Mockito框架

在单元测试过程中有时候Mock框架是必不可少的,通过Mock框架可以用来模拟对象的行为。这里我们以目前主流的Mockito框架进行介绍

abstract.png

POM依赖

这里我们向POM中引入Mockito依赖,同时这里对于Junit框架,我们选用Junit 5版本

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- Junit -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
</dependency>

<!-- Mockito -->
<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() {
// Mock的目标既可以是类、也可以是接口
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 );

// 参数 匹配 任何对象(包括null)
Mockito.when( list1.addAll( ArgumentMatchers.any() ) )
.thenReturn( true );
Assertions.assertTrue( list1.addAll(null) );
Assertions.assertTrue( list1.addAll(new HashSet()) );

// 对 boolean addAll(int index, Collection<? extends E> c) 方法进行打桩
// 要求: index参数匹配非null Integer整型, c参数匹配非null的List
Mockito.when( list1.addAll(ArgumentMatchers.anyInt(), ArgumentMatchers.anyList() ) )
.thenReturn( true );
Assertions.assertTrue( list1.addAll(24, new ArrayList()) );
Assertions.assertFalse( list1.addAll(24, new TreeSet()) );

// 参数 匹配 非null字符串
Mockito.when( list1.add( ArgumentMatchers.anyString() ))
.thenReturn(true);
Assertions.assertTrue( list1.add("Bob") );

// 参数 匹配 非null Integer整型
Mockito.when( list1.remove( ArgumentMatchers.anyInt() ) )
.thenReturn("删除指定位置上的元素成功");
Assertions.assertEquals( "删除指定位置上的元素成功", list1.remove(25) );

// 自定义参数匹配器, 参数 匹配 非null字符串, 且字符串长度不超过5
ArgumentMatcher<String> argumentMatcher = e -> e != null && e.length()<5;
// 通过argThat方法传入自定义参数匹配器
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));

// 链式调用 thenReturn 指定多个返回值, 调用时返回值会依次出现
// 当调用次数超过返回值的数量后,调用结果使用最后一个返回值
Mockito.when( list.get(1) )
.thenReturn(238) // 第1次调用时的返回值
.thenReturn(179) // 第2次调用时的返回值
.thenReturn(996); // 第3次、后续所有调用时的返回值
// 第1次调用
Assertions.assertEquals( 238, list.get(1) );
// 第2次调用
Assertions.assertEquals( 179, list.get(1));
// 第3次调用
Assertions.assertEquals( 996, list.get(1));
// 第4次调用
Assertions.assertEquals( 996, list.get(1) );

// 该方式等价于 链式调用 thenReturn 指定多个返回值
Mockito.when( list.get(2) )
.thenReturn(137, 12, 139);
// 第1次调用
Assertions.assertEquals( 137, list.get(2) );
// 第2次调用
Assertions.assertEquals( 12, list.get(2) );
// 第3次调用
Assertions.assertEquals( 139, list.get(2) );
// 第4次调用
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 );

// 链式调用 thenReturn 指定多个返回值, 调用时返回值会依次出现
// 当调用次数超过返回值的数量后,调用结果使用最后一个返回值
Mockito.when( list.get(1) )
.thenThrow( new RuntimeException("非法操作") ) // 第1次调用时抛出的异常
.thenReturn( 12 ) // 第2次调用时的返回值
.thenThrow( new RuntimeException("目标类不存在") ) // 第3次调用时抛出的异常
.thenReturn(23); // 第4次、后续所有调用时的返回值

// 第1次调用
Exception ex1 = Assertions.assertThrows(RuntimeException.class, ()->list.get(1) );
Assertions.assertEquals("非法操作", ex1.getMessage() );

// 第2次调用
Assertions.assertEquals( 12, list.get(1) );

// 第3次调用
Exception ex3 = Assertions.assertThrows(RuntimeException.class, ()->list.get(1) );
Assertions.assertEquals("目标类不存在", ex3.getMessage() );

// 第4次调用
Assertions.assertEquals( 23, list.get(1) );

// 第5次调用
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 );

// 调用then方法、并实现函数式接口Answer, 实现自定义返回值逻辑
// then方法、thenAnswer方法是相同的
Mockito.when( map.put(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt()) )
.then( invocation -> {
//.thenAnswer( invocation -> { // 也可通过thenAnswer方法实现相同的作用
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) // 第1次调用时的返回值
.doReturn(27) // 第2次、后续所有调用时的返回值
.when(list1)
.get(996);

// 第1次调用
Assertions.assertEquals( 14, list1.get(996) );
// 第2次调用
Assertions.assertEquals( 27, list1.get(996) );
// 第3次调用
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("今晚的月色真美")) // 第1次调用时抛出的异常
.doThrow( new RuntimeException("风也温柔") ) // 第2次、后续所有调用时抛出的异常
.when(list1)
.get(996);

// 第1次调用
Exception ex1 = Assertions.assertThrows( RuntimeException.class, ()->list1.get(996) );
Assertions.assertEquals("今晚的月色真美", ex1.getMessage() );

// 第2次调用
Exception ex2 = Assertions.assertThrows( RuntimeException.class, ()->list1.get(996) );
Assertions.assertEquals("风也温柔", ex2.getMessage() );

// 第3次调用
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 );

// 调用doAnswer方法、并实现函数式接口Answer, 实现自定义返回值逻辑
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() {
// 对于Spy的对象而言,其默认会调用真实方法
Person person2 = Mockito.spy(Person.class);
person2.hello("Bob");
// 通过 doNothing 实现对Void Method空方法进行打桩
// 使其不再会调用真实方法
Mockito.doNothing()
.when( person2 )
.hello( "奥特曼" );
person2.hello("奥特曼");
}

}

class Person {

...

public void hello(String name) {
System.out.println("Hello, I'm "+name);
}

...
}

测试结果如下,可以看到Void Method空方法一旦被打桩掉之后。原方法就不会再执行了,控制台也不会输出了

figure 1.jpeg

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);
// 对Mock的对象进行打桩
Mockito.doReturn(200)
.when( person1 )
.calc(13, 31);
Mockito.doReturn(500)
.when( person1 )
.calc(3, 10);
// 对Mock的对象进行打桩, 则调用后会返回打桩值
Assertions.assertEquals(200, person1.calc(13,31));
Assertions.assertEquals(500, person1.calc(3,10));
// 重置Mock的对象, 移除对其的所有打桩
Mockito.reset( person1 );
// 对于未打桩的方法, 则Mock的对象会根据方法的返回类型, 返回相应的空值
Assertions.assertEquals(0, person1.calc(13,31));
Assertions.assertEquals(0, person1.calc(3,10));

Person person2 = Mockito.spy(Person.class);
// 对Spy的对象进行打桩
Mockito.doReturn(200)
.when( person2 )
.calc(13, 31);
Mockito.doReturn(500)
.when( person2 )
.calc(3, 10);
// 对Spy的对象进行打桩, 则调用后会返回打桩值
Assertions.assertEquals(200, person2.calc(13,31));
Assertions.assertEquals(500, person2.calc(3,10));
// 重置Spy的对象, 移除对其的所有打桩
Mockito.reset( person2 );
// 对于未打桩的方法, 则Spy的对象会调用真实的方法
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 );

// Mock出来的对象, 如果调用未打桩的方法, 并不会调用真实的方法
// 其根据方法的返回类型, 返回相应的空值
Assertions.assertNull( animal1.getInfo("Aaron") );

// Mock出来的对象, 通过then系列方法进行打桩
// 并不会调用真实的方法, 其会直接返回相应的打桩值
Mockito.when( animal1.getInfo("Bob") )
.thenReturn("Hi, Bob");
Assertions.assertEquals("Hi, Bob", animal1.getInfo("Bob"));

// Mock出来的对象, 通过do系列方法进行打桩
// 并不会调用真实的方法, 其会直接返回相应的打桩值
Mockito.doReturn( "Hi, Tony" )
.when( animal1 )
.getInfo("Tony");
Assertions.assertEquals("Hi, Tony", animal1.getInfo("Tony"));


// Spy的对象是一个真实的对象
Animal animal2 = Mockito.spy( Animal.class );

// Spy出来的对象, 如果调用未打桩的方法, 其会调用真实的方法
// 并根据调用结果返回真实的返回值
Assertions.assertEquals("I'm 张三", animal2.getInfo("张三") );

// Spy出来的对象, 利用then系列方法进行打桩, 则会导致真实方法在打桩过程中被调用
Mockito.when( animal2.getInfo(ArgumentMatchers.anyString() ) )
.thenReturn("我是李四");
// 当打桩完毕后, 再调用该方法, 则不会调用真实方法
Assertions.assertEquals("我是李四", animal2.getInfo("李四"));

// Spy出来的对象, 利用do系列方法进行打桩, 则真实方法在打桩过程中不会被调用
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 {

/**
* 对Spy对象的方法进行打桩
*/
@Test
public void stubOnSpy() {
LinkedList<Integer> list1 = Mockito.spy( LinkedList.class );

/*************** thenReturn、doReturn ***********************/
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));

/**************** thenThrow、doThrow ************************/
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() );

/**************** then、thenAnswer、doAnswer ************************/
try{
Mockito.when( list1.get(5) )
//.thenAnswer( invocation -> 211 ); // 也可通过thenAnswer方法实现相同的作用
.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) );
}

}

测试结果如下所示

figure 2.jpeg

对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 {

/**
* 对Void Method空方法进行打桩
*/
@Test
public void stubOnVoidMethod() {
List<String> list1 = Mockito.mock(LinkedList.class);

// then系列方法无法对void方法进行打桩, 即下述代码会无法通过编译
// Mockito.when( list1.add(7, "Tony") )
// .thenThrow( new RuntimeException("今晚的月色真美") );

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
// 使@Mock、@Spy注解生效 方式1
@ExtendWith(MockitoExtension.class)
public class AnnotationDemo {

@Mock // 标识该对象是一个Mock的对象
private Man man;

@Spy // 标识该对象是一个Spy的对象
private Woman woman;

@InjectMocks // 将@Mock、@Spy修饰的对象自动注入到@InjectMocks修饰的对象中
@Spy // 标识该对象是一个Spy的对象
private Human human;

@BeforeEach
public void init() {
// 使@Mock、@Spy注解生效 方式2
//MockitoAnnotations.initMocks(this);
}

@Test
public void test1() {
// 验证man已经被成功Mock了
Assertions.assertNull( man.hello("Aaron") );
Mockito.doReturn( "你好, 我是男人" )
.when( man )
.hello( ArgumentMatchers.anyString() );
Assertions.assertEquals("你好, 我是男人", man.hello("Aaron") );

// 验证woman已经被成功Spy了
Assertions.assertEquals("Bye, I'm Tony", woman.bye("Tony") );
Mockito.doReturn( "你好, 我是女人" )
.when( woman )
.bye( ArgumentMatchers.anyString() );
Assertions.assertEquals("你好, 我是女人", woman.bye("Tony") );
}

/**
* 验证human已经被成功Spy、且相关依赖也注入成功
*/
@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;
}
}
请我喝杯咖啡捏~
  • 本文作者: Aaron Zhu
  • 本文链接: https://xyzghio.xyz/Mockito/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-ND 许可协议。转载请注明出处!

欢迎关注我的微信公众号:青灯抽丝