Mockito入门案例

2023/10/20 单元测试 共 12819 字,约 37 分钟

Mockito

依赖

与JUnit5整合

  • 推荐
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.6.0</version>
    <scope>test</scope>
</dependency>

核心包

  • 实际很少单独使用mockito,基本都与JUnit联用,如果JUnit版本为5,引入上一个节的包即可
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.6.0</version>
    <scope>test</scope>
</dependency>

验证

验证执行次数

  • Mockito.verify()方法有两个重载方法
    • <T> T verify(T mock)
      • 入参仅有一个mock对象,默认表示验证方法执行了一次
    • <T> T verify(T mock, VerificationMode mode)
      • 入参有mock对象和一个验证模式对象,根据传入验证模式对象不同,表现不同
  • 验证模式
    • 所有验证模式类均会实现VerificationMode接口
    • Mockito内置的验证模式可查看VerificationModeFactory类,其生产的对象均为Mockito提供的内置验证模式
    • Mockito类提供了一些工具方法返回一些验证模式
  • 以下示例代码及之后所有代码都静态导入了Mockito类的全部方法,如atMostOnce() mock() spy()实为Mockito.atMostOnce() Mockito.mock() Mockito.spy()
/**
* 验证执行次数
*/
@Test
void testVerifyTimes() {
    //mock实例
    List mockList = mock(List.class);

    //调用方法
    mockList.add(1);
    mockList.clear();
    //验证是否调用了一次
    verify(mockList).add(1);
    verify(mockList).clear();
    //至多1次
    verify(mockList, atMostOnce()).add(1);
    //至少1次
    verify(mockList, atLeastOnce()).add(1);

    //调用方法
    mockList.add(2);
    mockList.add(2);
    //验证多次调用
    verify(mockList, times(2)).add(2);
    //至少2次
    verify(mockList, atLeast(2)).add(2);
    //至多2次
    verify(mockList, atMost(2)).add(2);

    //从未被调用
    verify(mockList, never()).add(3);

}

顺序验证

  • 验证调用顺序是否如同期望的那样,示例代码如下
    • 单个mock对象方法的调用顺序
    • 多个mock对象方法的调用顺序
    • 仅能在顺序调用中表示最少调用次数的验证模式:Mockito.calls()的返回值Calls
@Test
void testInOrder() {
    // A. 验证 mock 一个对象的函数执行顺序
    List singleMock = mock(List.class);

    // 使用 singleMock
    singleMock.add("was added first");
    singleMock.add("was added second");

    // 为该 mock 对象创建一个 inOrder 对象
    InOrder inOrder = inOrder(singleMock);

    // 确保 add 函数首先执行的是 add("was added first"),然后才是 add("was added second")
    inOrder.verify(singleMock).add("was added first");
    inOrder.verify(singleMock).add("was added second");

    // B. 验证多个 mock 对象的函数执行顺序
    List firstMock = mock(List.class);
    List secondMock = mock(List.class);

    // 使用 mock
    firstMock.add("was called first");
    firstMock.add("was called first");
    firstMock.add("was called first");
    secondMock.add("was called second");

    // 为这两个 mock 对象创建 inOrder 对象
    InOrder inOrder2 = inOrder(firstMock, secondMock);

    // 验证它们的执行顺序
    inOrder2.verify(firstMock,calls(2)).add("was called first");
    inOrder2.verify(secondMock).add("was called second");

    //PS:calls(2)方法返回的验证模式表示至少2次,该方法仅用于顺序验证
}

确保交互(interaction)操作不会执行在 mock 对象上

  • Mockito官方文档中的交互(interactions)指对mock对象方法的调用
  • Mockito.nerver()返回结果本质是执行次数为0的验证模式
/**
* 验证mock对象没有未被verify的操作
*/
@Test
void testNoInteraction() {
    List mockOne = mock(List.class);
    List mockTwo = mock(List.class);
    List mockThree = mock(List.class);
    // 使用 Mock 对象
    mockOne.add("one");
    mockOne.add(1); //该处调用未被verify验证
    // 普通验证
    verify(mockOne).add("one");
    // 验证某个交互是否从未被执行
    verify(mockOne, never()).add("two");
}

查找冗余调用

  • 该实例中的API会列出所有交互了的方法并标记出未验证的方法
  • Mockito.verifyNoMoreInteractions()功能与Mockito.verifyZeroInteractions()()相同
  • Mockito.verifyNoMoreInteractions()Mockito.verifyZeroInteractions的替代方法
@Test
void testFindRedundant() {
    List mockedList = mock(List.class);
    // 使用 mock
    mockedList.add("one");
    mockedList.add("two");

    verify(mockedList).add("one");

    // 下面的验证将会失败
    verifyNoMoreInteractions(mockedList);
    
    // 验证 mock 对象没有交互过
    verifyZeroInteractions(mockedList);//过期方法
}

模拟

Mock模拟

编程实现Mockito.mock()
  • 返回的mock对象时Mockito根据参数的接口生成的子类
  • 为打桩的方法,默认情况下全部返回默认值(null、0、false等)
  • 入参字节码对象要求不能为final类
//mock实例
List mockList = Mockito.mock(List.class);
初始化注解环境
  • @ExtendWith(MockitoExtension.class) 注解方式
    • 写在需要使用到注解字段的方法上
    • 写在类上则整个测试类都开启了注解
  • MockitoAnnotations.initMocks(this) 编程方式
    • 需要在被mock对象调用前执行
@Mock
private List mockList;
@Test
//注解方式
@ExtendWith(MockitoExtension.class)
void testMock() {
    //MockitoAnnotations.initMocks(this); 编程方式
    //测试代码
}
注解实现@Mock
  • 基本功能及特性同Mockito.mock()
@Mock
private List mockList;
@Test
//注解方式
@ExtendWith(MockitoExtension.class)
void testMock() {
    when(mockList.get(0)).thenReturn("haha");
    assertEquals("haha", mockList.get(0));
    verify(mockList).get(0);
}

真实对象监控Spy

编程实现Mockito.spy()
  • 存在两个重载方法
    • 入参为字节码对象时
      • 底层调用同Mockito.mock()方法
      • 入参要求不能为final
    • 入参为真实对象时
//接口Class入参,同mock()方法
List spy = Mockito.spy(List.class);
//监控真实对象
List linkedList = new LinkedList();
List spy = Mockito.spy(linkedList);
@Test
void testSpy() {
    List list = new LinkedList();
    List spy = spy(list);
    // 你可以为某些函数打桩
    when(spy.size()).thenReturn(100);
    // 通过spy对象调用真实对象的函数
    spy.add("one");
    spy.add("two");
    // 输出第一个元素
    assertEquals("one", spy.get(0));//未打桩调用真实方法
    // 因为size()函数被打桩了,因此这里返回的是100
    assertEquals(100, spy.size());
    // 交互验证
    verify(spy).add("one");
    verify(spy).add("two");
}
spy对象打桩注意事项
  • 被spy的对象未打桩时会调用真实方法
    • 尽量使用doReturn()系列的方法打桩
    • when()系列方法中的调用,实际执行时会真的调用,第一次打桩时可能出问题,如下示例
@Test
void testSpyStub() {
    List list = new LinkedList();
    List spy = spy(list);
    
    when(spy.get(0)).thenReturn("foo");// 运行报错 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生IndexOutOfBoundsException异常,因为真实List对象是空的

    // 你需要使用doReturn()来打桩
    doReturn("foo").when(spy).get(0);
    assertEquals("foo", spy.get(0));
}
注解实现@Spy
  • 若被@Spy修改的字段未初始化实例,相当于@Mock和编程实现中的Mockito.mock()Mockito.spy(aClazz)
  • 若被@Spy修改的字段已初始化实例,相当于编程中的Mockito.spy(object)
@Mock
private List mockList;
@Test
//注解方式
@ExtendWith(MockitoExtension.class)
void testMock() {
    //MockitoAnnotations.initMocks(this); 编程方式
    //测试代码
}

真实局部mock

  • 同一个mock对象,部分方法mock,部分调用真实方法
    • spy对象方式:未打桩方法默认调用真实方法
    • mock方式:打桩时设置调用真实方法
/**
* 真实局部mock
*/
@Test
void realPartialMock() {
    //you can create partial mock with spy() method:
    //使用spy创建真实局部mock对象
    List list = spy(new LinkedList());

    //you can enable partial mock capabilities selectively on mocks:
    //使用mock实现真实局部mock功能
    Person mock = mock(Person.class);

    //打真实调用桩
    doCallRealMethod().when(mock).setName("Bob");
    when(mock.getName()).thenCallRealMethod();

    //验证
    mock.setName("Bob");
    assertEquals("Bob", mock.getName());
}

重置mock对象

  • 普通用户不推荐使用,容易引入代码以为,提供该方法主要是为了容器测试,而非提供给普通使用者
@Test
void testReset(){
    List mock = mock(List.class);
    when(mock.size()).thenReturn(10);
    mock.add(1);

    //重置并清空mock对象所有交互(即调用)和打桩
    reset(mock);
}

mock spy 抽象类

  • 需要mock的对象为抽象类,需要打桩的方法未实现,且本次打桩关心其实现法时,mockito提供直接mock抽象类的功能
    • 要求,抽象类必须有无参构造方法
/**
* mock spy 抽象类
*/
@Test
void testMockOrSpyAbstract() {
    //重载的spy方法
    SomeAbstract spy = spy(SomeAbstract.class);

    //设置生成器
    OtherAbstract mock = mock(OtherAbstract.class, withSettings()
                              .useConstructor().defaultAnswer(CALLS_REAL_METHODS));
    //mock一个非静态内部类
    OuterAbstract.InnerAbstract innerMock = mock(OuterAbstract.InnerAbstract.class, withSettings()                                     .useConstructor().outerInstance(outerAbstract).defaultAnswer(CALLS_REAL_METHODS));

}

private abstract static class SomeAbstract {
    public SomeAbstract() {
    }
}

private abstract static class OtherAbstract {
    public OtherAbstract() {
    }
}

private static class OuterAbstract {
    private abstract class InnerAbstract {
        public InnerAbstract() {
        }
    }
}

委托调用真实实例

  • 使用场景
    • 带有 interface 的 final 类
    • 已经自定义代理的对象
    • 带有 finalize 方法的特殊对象,就是避免重复执行
  • 和spy的区别
    • 标准的 spy包含被 spy 实例的所有状态信息,方法在 spy 对象上被调用。被 spy 的对象只在 mock 创建时被用来拷贝状态信息。如果你通过标准 spy 调用一个方法,这个 spy 会调用其内部的其他方法记录这次操作, 以便后面验证使用。等效于存根 (stubbed)操作。
    • mock delegates 只是简单的把所有方法委托给 delegate。delegate 一直被当成它代理的方法使用。如果你 从一个 mock 调用它被委托的方法,它会调用其内部方法,这些调用不会被记录,stubbing 在这里也不会生效。 Mock 的 delegates 相对于标准的 spy 来说功能弱了很多,不过在标准 spy 不能被创建的时候很有用。
/**
* 委托调用真实实例
*/
@Test
void testDelegatesTo() {
    CustomMockedList awesomeList = new CustomMockedList();

    List<String> mock = mock(List.class, delegatesTo(awesomeList));

    doReturn("String").when(mock).get(0);

    assertEquals("String", mock.get(0));

    verify(mock).get(0);

    assertEquals("null", awesomeList.get(0));
}

static final class CustomMockedList extends AbstractList<String> {

    public String get(int index) {
        return "null";
    }

    public int size() {
        return 0;
    }
}

打桩Stub

打桩基本概念与简单案例

  • 通过Mockito提供的API模拟方法的返回值的过程称作打桩,以方便测试
  • 调用mock对象未打桩的方法,若返回值为基本类型,返回默认值,若为class类返回null
/**
* 打桩
*/
@Test
void testStub() {
    // 可以 mock 具体的类型,不仅只是接口
    LinkedList mockedList = mock(LinkedList.class);

    //打桩
    when(mockedList.get(0)).thenReturn("first");
    when(mockedList.get(1)).thenThrow(new RuntimeException());
    
    //验证打桩结果
    assertEquals("first", mockedList.get(0));
    assertThrows(RuntimeException.class, () -> mockedList.get(1));

    //get(100)未打桩,返回null
    assertNull(mockedList.get(100));

    verify(mockedList).get(0);
}

给void方法打桩:更强大的打桩方式

  • doReturn()doThrow()doAnswer()doNothing()doCallRealMethod()
    • 同时支持给有返回值和无返回的方法打桩
    • 避免Spy监控对象调用真实方法带来的混乱(参见:spy对象打桩注意事项
    • 推荐方式
/**
* 给返回值为void的方法打桩,调用时抛出异常
*/
@Test
void testVoidMethod() {
    LinkedList mockedList = mock(LinkedList.class);
    doThrow(new RuntimeException()).when(mockedList).clear();

    assertThrows(RuntimeException.class, mockedList::clear);
}

连续调用打桩

  • 多次使用相同参数调用同一个mock对象相同的方法,返回不同的返回值
/**
* 连续调用打桩
*/
@Test
void stubConsecutiveCall() {
    TmpMock mock = mock(TmpMock.class);
    //连续调用mock方式一
    when(mock.someMethod("some arg"))
        .thenThrow(new RuntimeException())
        .thenReturn("foo");

    // 第一次调用 : 抛出运行时异常
    assertThrows(RuntimeException.class, () -> mock.someMethod("some arg"));

    // 第二次调用 : 输出 "foo"
    assertEquals("foo", mock.someMethod("some arg"));

    // 后续调用 : 也是输出 "foo"
    assertEquals("foo", mock.someMethod("some arg"));

    //连续调用mock方式二
    // 第一次调用时返回 "one",第二次返回 "two",第三次返回 "three"
    when(mock.someMethod("some arg"))
        .thenReturn("one", "two", "three");

    assertEquals("one", mock.someMethod("some arg"));
    assertEquals("two", mock.someMethod("some arg"));
    assertEquals("three", mock.someMethod("some arg"));
    
    //doReturn方法实现
    doReturn("haha").doThrow(RuntimeException.class).when(mockedList).get(0);

    assertEquals("haha", mockedList.get(0));
    assertThrows(RuntimeException.class, () -> mockedList.get(0));
}

使用Answer接口回调

  • thenReturn() thenThrow()功能足够,Answer接口方式仅供学习
/**
* 使用Answer接口回调打桩
*/
@Test
void stubWithCallBack() {
    TmpMock mock = mock(TmpMock.class);
    //匿名内部类
    when(mock.someMethod(anyString())).thenAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) {
            Object[] args = invocation.getArguments();
            Object mock = invocation.getMock();
            return "called with arguments: " + args[0];
        }
    });
    //lambda表达式
    when(mock.someMethod(anyString())).thenAnswer(invocation -> {
        Object[] args = invocation.getArguments();
        Object mock1 = invocation.getMock();
        return "called with arguments: " + args[0];
    });

    // 输出 : "called with arguments: foo"
    assertEquals("called with arguments: foo", mock.someMethod("foo"));
}

修改未打桩调用的默认返回值

  • 一般用不到,复杂的遗留系统可能需要
  • 实现Answer<T>接口,调用mock方法时传入实现类实例
/**
* 修改未打桩调用的默认返回值
*/
@Test
void modifyUnStubDefaultReturnValue() {
    List mock = mock(List.class, Mockito.RETURNS_SMART_NULLS);
    List mockTwo = mock(List.class, new YourOwnAnswer());


    assertEquals(0, mock.size());
    assertNull(mock.get(0));

    assertEquals(0, mockTwo.size());
    assertEquals("default value", mockTwo.get(0));
}

private static class YourOwnAnswer implements Answer<Object> {
    @Override
    public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
        Method method = invocationOnMock.getMethod();
        Class<?> returnType = method.getReturnType();
        Object res = null;
        switch (returnType.getName()) {
            case "java.lang.Object":
                res = "default value";
                break;
            default:
                res = 0;
                break;
        }
        return res;
    }
}

其它注解: @InjectMocks

  • @InjectMocks :自动将被mock或被spy的字段注入到被该注解修饰的对象中
@Mock
private Person person;
@InjectMocks
private Villa villa;

@Test
@ExtendWith(MockitoExtension.class)
void newAnnotation() {
    //验证@InjectMock注入效果
    when(person.getName()).thenReturn("Stark");
    assertEquals("Stark", villa.ownerName());
}

单行打桩

  • 一行代码同时实现打桩与生成mock对象
/**
* 单行打桩
*/
@Test
void inLineStub() {
    Person mock = when(mock(Person.class).getName())
        .thenReturn("ABC").getMock();
    assertEquals("ABC", mock.getName());
}

深度打桩

  • 如下示例,对于对象类嵌套对象,如果需要链式调用时,普通方式只能mock嵌套链上全部对象并对相应的方法打桩
  • mockito提供专门简化这种嵌套链式调用的打桩方式
    • 底层其实是修改默认返回值的实现方式,给链式调用中的方法提供了默认返回以支持链式打桩
/**
* 深度打桩
*/
@Test
void deepStub() {
    //普通方式给链式调用打桩
    City city = when(mock(City.class).getName()).thenReturn("Door").getMock();
    Address address = when(mock(Address.class).getCity()).thenReturn(city).getMock();
    User user = when(mock(User.class).getAddress()).thenReturn(address).getMock();

    assertEquals("Door", user.getAddress().getCity().getName());
    verify(city).getName();
    verify(address).getCity();
    verify(user).getAddress();
    
    // 使用RETURNS_DEEP_STUBS为链式调用打桩
    User mockUser = mock(User.class, RETURNS_DEEP_STUBS);
    when(mockUser.getAddress().getCity().getName()).thenReturn("Garden");

    // 现在,当你调用mockUser.getAddress().getCity().getName()时,应该返回"mockCity"
    assertEquals("Garden", mockUser.getAddress().getCity().getName());
}
class User {
    private Address address;
    // constructor, getters and setters...
    public Address getAddress() {
        return address;
    }
}

class Address {
    private City city;
    // constructor, getters and setters...
    public City getCity() {
        return city;
    }
}

class City {
    private String name;
    // constructor, getters and setters...
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

参数

参数匹配器

  • 方便对同一类型参数或符合自定义逻辑的参数打桩,避免重复编写打桩代码
  • ArgumentMatcher<T>接口为参数匹配器接口,实现该接口并配合Mockito.argThat()方法可自定义参数匹配器
  • Mockito提供了大量内置参数匹配器及相应的工具方法
/**
* 参数匹配:验证调用时传入的参数是否符合要求
*/
@Test
void testArgumentMatcher() {
    LinkedList mockedList = mock(LinkedList.class);
    // 使用内置的 anyInt() 参数匹配器
    when(mockedList.get(anyInt())).thenReturn("element");

    // 使用自定义的参数匹配器( 在isValid() 函数中返回你自己的匹配器实现 )
    when(mockedList.contains(argThat(new ArgumentMatcher<Object>() {
            @Override
            public boolean matches(Object o) {
                return o instanceof Integer;
            }
        }))).thenReturn(false);

    assertEquals("element",mockedList.get(2342432));
    assertTrue(mockedList.contains(234324));

    //参数匹配Integer,实际输入String,未mock方法返回默认值false(若返回值非基本类型,返回null)
    assertFalse(mockedList.contains("1.1"));
}

参数捕获

  • 方便在验证时捕获交互时的实际参数用于后续处理(assert验证等)
  • 实现方式
    • 编程实现
    • @Captor注解实现
//注解实现
@Captor
private ArgumentCaptor<Person> argument;
/**
* 参数捕获
*/
@Test
void captureArguments() {
    List spy = spy(List.class);
    //编程实现
    //由于注解是成员变量,此处为局部变量,同时使用并不会报错,但不建议
    // ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
    //调用方法
    spy.remove(new Person("John"));
    // 参数捕获
    verify(spy).remove(argument.capture());
    // 使用equal断言
    assertEquals("John", argument.getValue().getName());
}

文档信息

Search

    Table of Contents