单元测试之Mockito框架

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

单元测试之Mockito框架

abstract.png

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(24new ArrayList()) );
        Assertions.assertFalse( list1.addAll(24new 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(13712139);
        // 第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(137));
        Assertions.assertEquals(120, map.put(237));
        Assertions.assertEquals(10086, map.put(8610000));
        Assertions.assertEquals(520, map.put(996520));
    }
}

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空方法一旦被打桩掉之后。原方法就不会再执行了,控制台也不会输出了

单元测试之Mockito框架

figure 1.jpeg

reset 方法

reset 方法可以重置Mock、Spy的对象,移除对其的所有打桩

public class MockDemo1 {

    @Test
    public void testReset() {
        Person person1 = Mockito.mock(Person.class);
        // 对Mock的对象进行打桩
        Mockito.doReturn(200)
            .when( person1 )
            .calc(1331);
        Mockito.doReturn(500)
            .when( person1 )
            .calc(310);
        // 对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(1331);
        Mockito.doReturn(500)
            .when( person2 )
            .calc(310);
        // 对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) );
    }
    
}

测试结果如下所示

单元测试之Mockito框架

figure 2.jpeg

对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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!