学习单元测试 Mockito 一篇文章就够了

一、什么是 Mockito

Mockito 是一个强大的用于 Java 开发的模拟测试框架, 通过 Mockito 我们可以创建和配置 Mock 对象, 进而简化有外部依赖的类的测试.
使用 Mockito 的大致流程如下:

  • 创建外部依赖的 Mock 对象, 然后将此 Mock 对象注入到测试类中.
  • 执行测试代码.
  • 校验测试代码是否执行正确

二、为什么使用 Mockito

假设我们正在编写一个银行的服务 BankService, 这个服务的依赖关系如下:
在这里插入图片描述
当我们需要测试 BankService 服务时, 该真么办呢?

一种方法是构建真实的 BankDao, DB, AccountService 和 AuthService 实例, 然后注入到 BankService 中.

不用我说, 读者们也肯定明白, 这是一种既笨重又繁琐的方法, 完全不符合单元测试的精神. 那么还有一种更加优雅的方法吗?

自然是有的, 那就是我们今天的主角 Mock Object. 下面来看一下使用 Mock 对象后的框架图:
在这里插入图片描述
我们看到, BankDao, AccountService 和 AuthService 都被我们使用了虚拟的对象(Mock 对象) 来替换了, 因此我们就可以对 BankService 进行测试, 而不需要关注它的复杂的依赖了.

三、Mockito 基本使用

1. 导入 Maven 依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.4.0</version>
    <!--根据情况添加作用范围-->
    <scope>compile</scope>
</dependency>

2. 创建Mock对象

@Test
public void createMockObject() {
    // 使用 mock 静态方法创建 Mock 对象.
    List mockedList = mock(List.class);
    Assert.assertTrue(mockedList instanceof List);

    // mock 方法不仅可以 Mock 接口类, 还可以 Mock 具体的类型.
    ArrayList mockedArrayList = mock(ArrayList.class);
    Assert.assertTrue(mockedArrayList instanceof List);
    Assert.assertTrue(mockedArrayList instanceof ArrayList);
}

如上代码所示, 我们调用了 mock 静态方法来创建一个 Mock 对象. mock 方法接收一个 class 类型, 即我们需要 mock 的类型.

3. 配置 Mock 对象

当我们有了一个 Mock 对象后, 我们可以定制它的具体的行为. 例如:

import org.junit.Assert;
import org.junit.Test;
import java.util.Iterator;
import java.util.List;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TestJunit {
    @Test
    public void configMockObject() {
        List mockedList = mock(List.class);

        // 我们定制了当调用 mockedList.add("one") 时, 返回 true
        when(mockedList.add("one")).thenReturn(true);
        // 当调用 mockedList.size() 时, 返回 1
        when(mockedList.size()).thenReturn(1);

        Assert.assertTrue(mockedList.add("one"));
        // 因为我们没有定制 add("two"), 因此返回默认值, 即 false.
        Assert.assertFalse(mockedList.add("two"));
        Assert.assertEquals(mockedList.size(), 1);

        Iterator i = mock(Iterator.class);
        when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");
        String result = i.next() + " " + i.next();
        //assert
        Assert.assertEquals("Hello, Mockito!", result);
    }
}

我们使用 when(​...).thenReturn(​...) 方法链来定义一个行为, 例如

when(mockedList.add("one")).thenReturn(true)” 表示: 当调用了mockedList.add("one") 那么返回 true…

并且要注意的是, when(​...).thenReturn(​...) 方法链不仅仅要匹配方法的调用, 而且要方法的参数一样才行.

而且有趣的是, when(​...).thenReturn(​...) 方法链可以指定多个返回值, 当这样做后, 如果多次调用指定的方法, 那么这个方法会依次返回这些值.
例如 “when(i.next()).thenReturn("Hello,").thenReturn("Mockito!");”, 这句代码表示: 第一次调用 i.next() 时返回 “Hello,”, 第二次调用 i.next() 时返回 “Mockito!”.

4. Mock 抛出异常

上面的例子我们展示了方法调用返回值的定制, 那么我们可以指定一个抛出异常吗?

当然可以的, 例如:

@Test(expected = NoSuchElementException.class)
public void testForIOException() throws Exception {
    Iterator i = mock(Iterator.class);
    when(i.next()).thenReturn("Hello,").thenReturn("Mockito!"); // 1
    String result = i.next() + " " + i.next(); // 2
    Assert.assertEquals("Hello, Mockito!", result);

    doThrow(new NoSuchElementException()).when(i).next(); // 3
    i.next(); // 4
}

上面代码的第一第二步我们已经很熟悉了, 接下来第三部我们使用了一个新语法:

doThrow(ExceptionX).when(x).methodCall, 它的含义是: 当调用了 x.methodCall 方法后, 抛出异常 ExceptionX.

因此 doThrow(new NoSuchElementException()).when(i).next() 的含义就是: 当第三次调用 i.next() 后, 抛出异常 NoSuchElementException.(因为 i 这个迭代器只有两个元素)

四、检验 Mock 对象的方法调用

Mockito 会追踪 Mock 对象的所用方法调用和调用方法时所传递的参数. 我们可以通过 verify() 静态方法来来校验指定的方法调用是否满足断言. 语言描述有一点抽象, 下面我们仍然以代码来说明一下.

@Test
public void testVerify() {
    List mockedList = mock(List.class);
    mockedList.add("one");
    mockedList.add("two");
    mockedList.add("three times");
    mockedList.add("three times");
    mockedList.add("three times");
    when(mockedList.size()).thenReturn(5);
    Assert.assertEquals(mockedList.size(), 5);

    verify(mockedList, atLeastOnce()).add("one");
    verify(mockedList, times(1)).add("two");
    verify(mockedList, times(3)).add("three times");
    verify(mockedList, never()).isEmpty();
}

上面的例子前半部份没有什么特别的, 我们关注后面的:

verify(mockedList, atLeastOnce()).add("one");
verify(mockedList, times(1)).add("two");
verify(mockedList, times(3)).add("three times");
verify(mockedList, never()).isEmpty();

读者根据代码也应该可以猜测出它的含义了, 很简单:

  • 第一句校验 mockedList.add(“one”) 至少被调用了 1 次(atLeastOnce)
  • 第二句校验 mockedList.add(“two”) 被调用了 1 次(times(1))
  • 第三句校验 mockedList.add(“three times”) 被调用了 3 次(times(3))
  • 第四句校验 mockedList.isEmpty() 从未被调用(never)

五、使用 spy() 部分模拟对象

Mockito 提供的 spy 方法可以包装一个真实的 Java 对象, 并返回一个包装后的新对象. 若没有特别配置的话, 对这个新对象的所有方法调用, 都会委派给实际的 Java 对象. 例如:

@Test
public void testSpy() {
    List list = new LinkedList();
    List spy = spy(list);

    // 对 spy.size() 进行定制.
    when(spy.size()).thenReturn(100);

    spy.add("one");
    spy.add("two");

    // 因为我们没有对 get(0), get(1) 方法进行定制,
    // 因此这些调用其实是调用的真实对象的方法.
    Assert.assertEquals(spy.get(0), "one");
    Assert.assertEquals(spy.get(1), "two");

    Assert.assertEquals(spy.size(), 100);
}

这个例子中我们实例化了一个 LinkedList 对象, 然后使用 spy() 方法对 list 对象进行部分模拟.

接着我们使用 when(...).thenReturn(...) 方法链来规定 spy.size() 方法返回值是 100. 随后我们给 spy 添加了两个元素, 然后再 调用 spy.get(0) 获取第一个元素.

这里有意思的地方是: 因为我们没有定制 add(“one”), add(“two”), get(0), get(1), 因此通过 spy 调用这些方法时, 实际上是委派给 list 对象来调用的.

然而我们 定义了 spy.size() 的返回值, 因此当调用 spy.size() 时, 返回 100.

六、常用操作

1. 验证mock对象是否执行了某些操作

    list.add(1);
    list.add(2);
    Mockito.verify(list).add(1);//验证通过
    Mockito.verify(list).add(5);//验证未通过,因为没有执行过该操作

2. 打桩

打桩就是模拟一些函数调用的反应。

    Mockito.when(list.get(1)).thenReturn(3);
    Assert.assertEquals(list.get(1),3);
    Mockito.when(list.get(1)).thenReturn(4);
    Assert.assertEquals(list.get(1),4);

默认情况下,对于有返回值的方法,Mock返回null,原始/原始包装器值或空集合。

例如,对于int/Integer为0,对于boolean/Boolean为false。

可以进行多次打桩,但是以最后一次为准。

一次打桩,可多次调用

3. 参数匹配器

    Mockito.when(list.get(Mockito.anyInt())).thenReturn("hi");//只要传入任何int,返回hi
    Assert.assertEquals("hi",list.get(1));//hi
    Assert.assertEquals("hi",list.get(999));//hi
    Mockito.when(list.contains(Mockito.isA(One.class))).thenReturn(true);//只要传入String类型,就返回hello
    Assert.assertTrue(list.contains("hello"));//hello
    Assert.assertTrue(list.contains(1));//发生错误

参数匹配器是为了更加灵活的进行验证和打桩,可以自定义

注意:如果在一个方法中使用参数匹配器,那么该方法的所有参数都要使用参数匹配器,验证和打桩都是如此,例如

    A a=Mockito.mock(A.class);
    //Mockito.when(a.add(Mockito.anyInt(),1)).thenReturn(2);//出错
    Mockito.when(a.add(Mockito.anyInt(),Mockito.anyInt())).thenReturn(2);//成功

4. 验证方法的调用次数/最多/最少/从不等

不传入该参数,默认是1

    public void verifyNumber(){
        List list=Mockito.mock(List.class);
        list.add(1);
        list.add(1);
        list.add(1);
        list.add(1);
        list.remove(3);
        list.remove(3);
        list.remove(3);
        list.get(3);
        //验证调用次数
        Mockito.verify(list,Mockito.times(4)).add(1);
        Mockito.verify(list,Mockito.times(3)).remove(3);
        Mockito.verify(list,Mockito.times(0)).add(0);
        Mockito.verify(list).add(1);//异常

        Mockito.verify(list,Mockito.atLeast(1)).add(1);//至少
        Mockito.verify(list,Mockito.atMost(5)).add(1);//之多
        Mockito.verify(list,Mockito.never()).get(0);//没有调用
        Mockito.verify(list,Mockito.atMostOnce()).get(3);//最多
    }

5. 返回值为void的方法调用时抛异常打桩

如下测试通过

    @Test(expected = RuntimeException.class)
    public void throwTest(){
        List list=Mockito.mock(List.class);
        //此处抛异常
        Mockito.doThrow(new RuntimeException("异常")).when(list).add(1,1);
        list.add(1, 1);
    }

6. 验证执行顺序

    public void orderTest(){
        List one = Mockito.mock(List.class);
        one.add(1);
        one.add(4);
        one.add(2);
        //创建一个顺序验证器
        InOrder inOrder=Mockito.inOrder(one);
        //验证执行顺序,不一致则抛异常
        inOrder.verify(one).add(1);
        inOrder.verify(one).add(2);
        
        //验证多个mock的调用顺序
        List list=Mockito.mock(List.class);
        A a=Mockito.mock(A.class);
        list.add(1);
        a.add(1,2);
        InOrder inOrder1=Mockito.inOrder(list,a);
        inOrder1.verify(list).add(1);
        inOrder1.verify(a).add(1,2);
    }

先后顺序一致则可,不必死板的验证每一次调用

7. 验证没有任何交互

    List list=Mockito.mock(List.class);
    list.add(1);
    List list2=Mockito.mock(List.class);
    List list3=Mockito.mock(List.class);
    Mockito.verifyNoInteractions(list2,list3);
    Mockito.verifyNoInteractions(list,list2);//异常

8. 检查是否还有没有验证的交互

它用来检查有么有冗余的调用

        List list = Mockito.mock(List.class);
        list.add(1);
        Mockito.verify(list).add(1);
        Mockito.verifyNoMoreInteractions(list);
        list.add(2);
        Mockito.verifyNoMoreInteractions(list);//异常

9. 简化Mock创建,注解驱动

@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {
    //注解取代Mockito.mock(A.class)
    @Mock
    private A myA;
    //在使用注解前需要执行MockitoAnnotations.openMocks(testClass.class);
    //否则无法使用
    @BeforeClass
    public static void start(){
        MockitoAnnotations.openMocks(MockitoTest.class);
    }
    @Test
    public void testAnnotation(){
        myA.abc();
    }
}

10. 连续打桩

    Demo mock=Mockito.mock(Demo.class);
    Mockito.when(mock.someMethod("some arg"))
            .thenReturn("one", "two", "three");
    System.out.println(mock.someMethod("some arg"));//one
    System.out.println(mock.someMethod("some arg"));//two
    System.out.println(mock.someMethod("some arg"));//three
    System.out.println(mock.someMethod("some arg"));//three

最后的将持续下去

11. 回调打桩

    Demo demo=Mockito.mock(Demo.class);
    Mockito.when(demo.someMethod(Mockito.any())).thenAnswer(new Answer<Object>() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Object[] arguments = invocation.getArguments();
            Object mock = invocation.getMock();
            return mock.toString()+" "+arguments.length+" "+arguments;
        }
    });
    System.out.println(demo.someMethod("he"));

12. doReturn()、doThrow()、doAnswer()、doNothing()、doCallRealMethod()系列方法的运用

对于有返回值的方法进行打桩Mockito.when(instance.someMethod()).thenReturn(xxx),但是对于没有返回值的方法却不能放在括号内当参数传递,因此对于它们有不同的方法进行打桩。

@Test(expected = RuntimeException.class)
public void voidTest() {
    List list=Mockito.mock(List.class);
    Mockito.doThrow(new RuntimeException()).when(list).clear();
    list.clear();
}

13. spy 监视真实的对象

在spy上执行方法时如果没有打桩,那么在真实的对象上也会执行该方法,尽量少用

public void spyTest(){
        List list=new LinkedList();
        List spy = Mockito.spy(list);
        spy.add("a");
        spy.add("b");
        Mockito.when(spy.size()).thenReturn(5);
        System.out.println(spy.size());
        System.out.println(list.size());

    }

14. detail

获取mock的详情

public void detailTest(){
    List list = Mockito.mock(List.class);
    list.add(1);
    Mockito.verify(list).add(1);
    Mockito.verifyNoMoreInteractions(list);
    list.add(2);
    MockingDetails mockingDetails = Mockito.mockingDetails(list);
    System.out.println(mockingDetails.getInvocations());
    System.out.println(mockingDetails.getStubbings());
    System.out.println(mockingDetails.isMock());
    System.out.println(mockingDetails.isSpy());
}
/*
[list.add(1);, list.add(2);]
[]
true
false
*/

15. 自定义失败打印信息

    public void desc(){
        List list=Mockito.mock(List.class);
        list.add(1);
        Mockito.verify(list,Mockito.description("失败了")).add(1);
        Mockito.verify(list,Mockito.description("失败了")).add(2);
    }

16. lambda支持

 verify(list, times(2)).add(argThat(string -> string.length() < 5));
 //等同于
  verify(list, times(2)).add(argThat(new ArgumentMatcher(){
     public boolean matches(String arg) {
         return arg.length() < 5;
     }
 }));

【参考】

【1】https://blog.csdn.net/gentlezuo/article/details/108293961
【2】https://segmentfault.com/a/1190000006746409

  • 18
    点赞
  • 93
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值