在单元测试过程中有时候Mock框架是必不可少的,通过Mock框架可以用来模拟对象的行为。这里我们以目前主流的Mockito框架进行介绍
POM依赖
这里我们向POM中引入Mockito依赖,同时这里对于Junit框架,我们选用Junit 5版本
<!-- 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的目标既可以是类、也可以是接口
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方法传入自定义参数匹配器即可
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可以实现对方法进行打桩以返回指定值
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可以实现对方法进行打桩,以实现对方法调用抛出指定异常
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 方法作用是相同的
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 方法也可以实现对方法进行打桩以返回指定值
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 方法也可以实现对方法进行打桩,以实现对方法调用抛出指定异常
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,并实现自定义返回值逻辑即可
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时,不会再调用真实的方法
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空方法一旦被打桩掉之后。原方法就不会再执行了,控制台也不会输出了
reset 方法
reset 方法可以重置Mock、Spy的对象,移除对其的所有打桩
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系列方法进行打桩, 则真实方法在打桩过程中不会被调用
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系列方法进行打桩
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) );
}
}
测试结果如下所示
对Void Method进行打桩
then系列方法无法对Void Method方法进行打桩,其会导致无法通过编译。故此时也只能选择do系列方法进行打桩
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方法注入
// 使@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;
}
}
原文始发于微信公众号(青灯抽丝):单元测试之Mockito框架
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/156665.html