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

Posted 南淮北安

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了学习单元测试 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. 检查是否还有没有验证的交互

它用来检查有

以上是关于学习单元测试 Mockito 一篇文章就够了的主要内容,如果未能解决你的问题,请参考以下文章

学习单元测试常用的断言 assertTrue 和 assertEquals 一篇文章就够了

Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片

Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片

(转) TensorFlow深度学习,一篇文章就够了

学习 ES6,一篇文章就够了

学习Python装饰器,看这一篇文章就够了