一文让你快速上手 Mockito 单元测试框架

前言spring

在计算机编程中,单元测试是一种软件测试方法,经过该方法能够测试源代码的各个单元功能是否适合使用。为代码编写单元测试有不少好处,包括能够及早的发现代码错误,促进更改,简化集成,方便代码重构以及许多其它功能。使用 Java 语言的朋友应该用过或者听过 Junit 就是用来作单元测试的,那么为何咱们还须要 Mockito 测试框架呢?想象一下这样的一个常见的场景,当前要测试的类依赖于其它一些类对象时,若是用 Junit 来进行单元测试的话,咱们就必须手动建立出这些依赖的对象,这实际上是个比较麻烦的工做,此时就可使用 Mockito 测试框架来模拟那些依赖的类,这些被模拟的对象在测试中充当真实对象的虚拟对象或克隆对象,并且 Mockito 同时也提供了方便的测试行为验证。这样就可让咱们更多地去关注当前测试类的逻辑,而不是它所依赖的对象。数据库

1编程

生成 Mock 对象方式框架

要使用 Mockito,首先须要在咱们的项目中引入 Mockito 测试框架依赖,基于 Maven 构建的项目引入以下依赖便可:ide

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.3.3</version>
    <scope>test</scope>
</dependency>

若是是基于 Gradle 构建的项目,则引入以下依赖:工具

testCompile group: 'org.mockito', name: 'mockito-core', version: '3.3.3'

使用 Mockito 一般有两种常见的方式来建立 Mock 对象。单元测试

1.1学习

使用 Mockito.mock(clazz) 方式测试

经过 Mockito 类的静态方法 mock 来建立 Mock 对象,例如如下建立了一个 List 类型的 Mock 对象:this

List<String> mockList = Mockito.mock(ArrayList.class);

因为 mock 方法是一个静态方法,因此一般会写成静态导入方法的方式,即 ListmockList = mock(ArrayList.class)。

1.2

使用 @Mock 注解方式

第二种方式就是使用 @Mock 注解方式来建立 Mock 对象,使用该方式创须要注意的是要在运行测试方法前使用 MockitoAnnotations.initMocks(this) 或者单元测试类上加上 @ExtendWith(MockitoExtension.class) 注解,以下所示代码建立了一个 List 类型的 Mock 对象(PS: @BeforeEach 是 Junit 5 的注解,功能相似于 Junit 4 的 @Before 注解。):

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
//@ExtendWith(MockitoExtension.class)
public class MockitoTest {

  @Mock
  private List<String> mockList;

  @BeforeEach
  public void beforeEach() {
    MockitoAnnotations.initMocks(this);
  }
}

2

验证性测试

Mockito 测试框架中提供了 Mockito.verify 静态方法让咱们能够方便的进行验证性测试,好比方法调用验证、方法调用次数验证、方法调用顺序验证等,下面看看具体的代码。

2.1

验证方法单次调用

验证方法单次调用的话直接 verify 方法后加上待验证调用方法便可,如下代码的功能就是验证 mockList 对象的 size 方法被调用一次。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_SimpleInvocationOnMock() {
    mockList.size();
    verify(mockList).size();
  }
}

2.2

验证方法调用指定次数

除了验证单次调用,咱们有时候还须要验证一些方法被调用屡次或者指定的次数,那么此时就可使用 verify + times 方法来验证方法调用指定次数,同时还能够结合 atLeast + atMost 方法来提供调用次数范围,同时还有 never 等方法验证不被调用等。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_NumberOfInteractionsWithMock() {
    mockList.size();
    mockList.size();

    verify(mockList, times(2)).size();
    verify(mockList, atLeast(1)).size();
    verify(mockList, atMost(10)).size();
  }
}

2.3

验证方法调用顺序

同时还可使用 inOrder 方法来验证方法的调用顺序,下面示例验证 mockList 对象的 size、add 和 clear 方法的调用顺序。

/**
 * @author mghio
 * @date: 2020-05-28
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoVerifyTest {

  @Mock
  List<String> mockList;

  @Test
  void verify_OrderedInvocationsOnMock() {
    mockList.size();
    mockList.add("add a parameter");
    mockList.clear();

    InOrder inOrder = inOrder(mockList);

    inOrder.verify(mockList).size();
    inOrder.verify(mockList).add("add a parameter");
    inOrder.verify(mockList).clear();
  }
}

以上只是列举了一些简单的验证性测试,还有验证测试方法调用超时以及更多的验证测试能够经过相关官方文档探索学习。

3

验证方法异常

异常测试咱们须要使用 Mockito 框架提供的一些调用行为定义,Mockito 提供了 when(...).thenXXX(...) 来让咱们定义方法调用行为,如下代码定义了当调用 mockMap 的 get 方法不管传入任何参数都会抛出一个空指针 NullPointerException 异常,而后经过 Assertions.assertThrows 来验证调用结果。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@ExtendWith(MockitoExtension.class)
public class MockitoExceptionTest {

  @Mock
  public Map<String, Integer> mockMap;

  @Test
  public void whenConfigNonVoidReturnMethodToThrowEx_thenExIsThrown() {
    when(mockMap.get(anyString())).thenThrow(NullPointerException.class);

    assertThrows(NullPointerException.class, () -> mockMap.get("mghio"));
  }
}

同时 when(...).thenXXX(...) 不只能够定义方法调用抛出异常,还能够定义调用方法后的返回结果,好比 when(mockMap.get("mghio")).thenReturn(21); 定义了当咱们调用 mockMap 的 get 方法并传入参数 mghio 时的返回结果是 21。这里有一点须要注意,使用以上这种方式定义的 mock 对象测试实际并不会影响到对象的内部状态,以下图所示:

一文让你快速上手 Mockito 单元测试框架

虽然咱们已经在 mockList 对象上调用了 add 方法,可是实际上 mockList 集合中并无加入 mghio,这时候若是须要对 mock 对象有影响,那么须要使用 spy 方式来生成 mock 对象。

public class MockitoTest {

  private List<String> mockList = spy(ArrayList.class);

  @Test
  public void add_spyMockList_thenAffect() {
    mockList.add("mghio");

    assertEquals(0, mockList.size());
  }
}

断点后能够发现当使用 spy 方法建立出来的 mock 对象调用 add 方法后,mghio 被成功的加入到 mockList 集合当中。

4


与 Spring 框架集成

Mockito 框架提供了 @MockBean 注解用来将 mock 对象注入到 Spring 容器中,该对象会替换容器中任何现有的相同类型的 bean,该注解在须要模拟特定 bean(例如外部服务)的测试场景中颇有用。若是使用的是 Spring Boot 2.0+ 而且当前容器中已有相同类型的 bean 的时候,须要设置 spring.main.allow-bean-definition-overriding 为 true(默认为 false)容许 bean 定义覆盖。下面假设要测试经过用户编码查询用户的信息,有一个数据库操做层的 UserRepository,也就是咱们等下要 mock 的对象,定义以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Repository
public interface UserRepository {

  User findUserById(Long id);

}

还有用户操做的相关服务 UserService 类,其定义以下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@Service
public class UserService {

  private UserRepository userRepository;

  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  public User findUserById(Long id) {
    return userRepository.findUserById(id);
  }
}

在测试类中使用 @MockBean 来标注 UserRepository 属性表示这个类型的 bean 使用的是 mock 对象,使用 @Autowired 标注表示 UserService 属性使用的是 Spring 容器中的对象,而后使用 @SpringBootTest 启用 Spring 环境便可。

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
@SpringBootTest
public class UserServiceUnitTest {

  @Autowired
  private UserService userService;

  @MockBean
  private UserRepository userRepository;

  @Test
  public void whenUserIdIsProvided_thenRetrievedNameIsCorrect() {
    User expectedUser = new User(9527L, "mghio", "18288888880");
    when(userRepository.findUserById(9527L)).thenReturn(expectedUser);
    User actualUser = userService.findUserById(9527L);
    assertEquals(expectedUser, actualUser);
  }
}

5

Mockito 框架的工做原理

经过以上介绍能够发现, Mockito 很是容易使用而且能够方便的验证一些方法的行为,相信你已经看出来了,使用的步骤是先建立一个须要 mock 的对象 Target ,该对象以下:

public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

而后咱们直接使用 Mockito.mock 方法和 when(...).thenReturn(...) 来生成 mock 对象并指定方法调用时的行为,代码以下:

@Test
public void test_foo() {
  String expectedResult = "Mocked mghio";
  when(mockTarget.foo("mghio")).thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

仔细观察以上 when(mockTarget.foo("mghio")).thenReturn(expectedResult) 这行代码,首次使用我也以为很奇怪,when 方法的入参居然是方法的返回值 mockTarget.foo("mghio"),以为正确的代码应该是这样 when(mockTarget).foo("mghio"),可是这个写法实际上没法进行编译。既然 Target.foo 方法的返回值是 String 类型,那是否是可使用以下方式呢?

Mockito.when("Hello, I am mghio").thenReturn("Mocked mghio");

结果是编译经过,可是在运行时报错:

一文让你快速上手 Mockito 单元测试框架

从错误提示能够看出,when 方法须要一个方法调用的参数,实际上它只须要 more 对象方法调用在 when 方法以前就行,咱们看看下面这个测试代码:

@Test
public void test_mockitoWhenMethod() {
  String expectedResult = "Mocked mghio";
  mockTarget.foo("mghio");
  when("Hello, I am mghio").thenReturn(expectedResult);
  String actualResult = mockTarget.foo("mghio");
  assertEquals(expectedResult, actualResult);
}

以上代码能够正常测试经过,结果以下:

一文让你快速上手 Mockito 单元测试框架

为何这样就能够正常测试经过?是由于当咱们调用 mock 对象的 foo 方法时,Mockito 会拦截方法的调用而后将方法调用的详细信息保存到 mock 对象的上下文中,当调用到 Mockito.when 方法时,其实是从该上下文中获取最后一个注册的方法调用,而后把 thenReturn 的参数做为其返回值保存,而后当咱们的再次调用 mock 对象的该方法时,以前已经记录的方法行为将被再次回放,该方法触发拦截器从新调用而且返回咱们在 thenReturn 方法指定的返回值。如下是 Mockito.when 方法的源码:

一文让你快速上手 Mockito 单元测试框架

该方法里面直接使用了 MockitoCore.when 方法,继续跟进,该方法源码以下:

一文让你快速上手 Mockito 单元测试框架

仔细观察能够发现,在源码中并无用到参数 methodCall,而是从 MockingProgress 实例中获取 OngoingStubbing 对象,这个 OngoingStubbing 对象就是前文所提到的上下文对象。我的感受是 Mockito 为了提供简洁易用的 API 而后才制造了 when 方法调用的这种“幻象”,简而言之,Mockito 框架经过方法拦截在上下文中存储和检索方法调用详细信息来工做的。

6


如何实现一个微型的 Mock 框架***

知道了 Mockito 的运行原理以后,接下来看看要如何本身去实现一个相似功能的 mock 框架出来,看到方法拦截这里我相信你已经知道了,其实这就是 AOP 啊,可是经过阅读其源码发现 Mockito 其实并无使用咱们熟悉的 Spring AOP 或者 AspectJ 作的方法拦截,而是经过运行时加强库 Byte Buddy 和反射工具库 Objenesis 生成和初始化 mock 对象的。如今,经过以上分析和源码阅读能够定义出一个简单版本的 mock 框架了,将自定义的 mock 框架命名为 imock。这里有一点须要注意的是,Mockito 有一个好处是,它不须要进行初始化,能够直接经过其提供的静态方法来当即使用它。在这里咱们也使用相同名称的静态方法,经过 Mockito 源码:
一文让你快速上手 Mockito 单元测试框架
很容易看出 Mockito 类最终都是委托给 MockitoCore 去实现的功能,而其只提供了一些面向使用者易用的静态方法,在这里咱们也定义一个这样的代理对象 IMockCore,这个类中须要一个建立 mock 对象的方法 mock 和一个给方法设定返回值的 thenReturn 方法,同时该类中持有一个方法调用详情 InvocationDetail 集合列表,这个类是用来记录方法调用详细信息的,而后 when 方法仅返回列表中的最后一个 InvocationDetail,这里列表能够直接使用 Java 中经常使用的 ArrayList 便可,这里的 ArrayList 集合列表就实现了 Mockito 中的 OngoingStubbing 的功能。根据方法的三要素方法名、方法参数和方法返回值很容易就能够写出 InvocationDetail 类的代码,为了对方法在不一样类有同名的状况区分,还须要加上类全称字段和重写该类的 equals 和 hashCode 方法(判断是否在调用方法集合列表时须要根据该方法判断),代码以下所示:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class InvocationDetail<T> {

  private String attachedClassName;

  private String methodName;

  private Object[] arguments;

  private T result;

  public InvocationDetail(String attachedClassName, String methodName, Object[] arguments) {
    this.attachedClassName = attachedClassName;
    this.methodName = methodName;
    this.arguments = arguments;
  }

  public void thenReturn(T t) {
    this.result = t;
  }

  public T getResult() {
    return result;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    InvocationDetail<?> behaviour = (InvocationDetail<?>) o;
    return Objects.equals(attachedClassName, behaviour.attachedClassName) &&
        Objects.equals(methodName, behaviour.methodName) &&
        Arrays.equals(arguments, behaviour.arguments);
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(attachedClassName, methodName);
    result = 31 * result + Arrays.hashCode(arguments);
    return result;
  }
}

接下来就是如何去建立咱们的 mock 对象了,在这里咱们也使用 Byte Buddy 和 Objenesis 库来建立 mock 对象,IMockCreator 接口定义以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public interface IMockCreator {

  <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList);

}

实现类 ByteBuddyIMockCreator 使用 Byte Buddy 库在运行时动态生成 mock 类对象代码而后使用 Objenesis 去实例化该对象。代码以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class ByteBuddyIMockCreator implements IMockCreator {

  private final ObjenesisStd objenesisStd = new ObjenesisStd();

  @Override
  public <T> T createMock(Class<T> mockTargetClass, List<InvocationDetail> behaviorList) {
    ByteBuddy byteBuddy = new ByteBuddy();

    Class<? extends T> classWithInterceptor = byteBuddy.subclass(mockTargetClass)
        .method(ElementMatchers.any())
        .intercept(MethodDelegation.to(InterceptorDelegate.class))
        .defineField("interceptor", IMockInterceptor.class, Modifier.PRIVATE)
        .implement(IMockIntercepable.class)
        .intercept(FieldAccessor.ofBeanProperty())
        .make()
        .load(getClass().getClassLoader(), Default.WRAPPER).getLoaded();

    T mockTargetInstance = objenesisStd.newInstance(classWithInterceptor);
    ((IMockIntercepable) mockTargetInstance).setInterceptor(new IMockInterceptor(behaviorList));

    return mockTargetInstance;
  }
}

基于以上分析咱们能够很容易写出建立 mock 对象的 IMockCore 类的代码以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockCore {

  private final List<InvocationDetail> invocationDetailList = new ArrayList<>(8);

  private final IMockCreator mockCreator = new ByteBuddyIMockCreator();

  public <T> T mock(Class<T> mockTargetClass) {
    T result = mockCreator.createMock(mockTargetClass, invocationDetailList);
    return result;
  }

  @SuppressWarnings("unchecked")
  public <T> InvocationDetail<T> when(T methodCall) {
    int currentSize = invocationDetailList.size();
    return (InvocationDetail<T>) invocationDetailList.get(currentSize - 1);
  }
}

提供给使用者的类 IMock 只是对 IMockCore 进行的简单调用而已,代码以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMock {

  private static final IMockCore IMOCK_CORE = new IMockCore();

  public static <T> T mock(Class<T> clazz) {
    return IMOCK_CORE.mock(clazz);
  }

  public static <T> InvocationDetail when(T methodCall) {
    return IMOCK_CORE.when(methodCall);
  }
}

经过以上步骤,咱们就已经实现了一个微型的 mock 框架了,下面来个实际例子测试一下,首先建立一个 Target 对象:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class Target {

  public String foo(String name) {
    return String.format("Hello, %s", name);
  }

}

而后编写其对应的测试类 IMockTest 类以下:

/**
 * @author mghio
 * @date: 2020-05-30
 * @version: 1.0
 * @description:
 * @since JDK 1.8
 */
public class IMockTest {

  @Test
  public void test_foo_method() {
    String exceptedResult = "Mocked mghio";
    Target mockTarget = IMock.mock(Target.class);

    IMock.when(mockTarget.foo("mghio")).thenReturn(exceptedResult);

    String actualResult = mockTarget.foo("mghio");

    assertEquals(exceptedResult, actualResult);
  }

}

以上测试的能够正常运行,达到了和 Mockito 测试框架同样的效果,运行结果以下:

一文让你快速上手 Mockito 单元测试框架
上面只是列出了一些关键类的源码,自定义 IMock 框架的全部代码已上传至 Github 仓库 imock,感兴趣的朋友能够去看看。


7

总结

本文只是介绍了 Mockito 的一些使用方法,这只是该框架提供的最基础功能,更多高级的用法能够去官网阅读相关的文档,而后介绍了框架中 when(...).thenReturn(...) 定义行为方法的实现方式并按照其源码思路实现了一个相同功能的简易版的 imock。虽然进行单元测试有不少优势,可是也不可盲目的进行单元测试,在大部分状况下咱们只要作好对项目中逻辑比较复杂、不容易理解的核心业务模块以及项目中公共依赖的模块的单元测试就能够了。

相关文章
相关标签/搜索