单元测试之Mock

1-Mock详解

1.1-什么是Mock?

mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。 Mock有很多的实现框架,例如Mockito、EasyMock、Jmockit、PowerMock、Spock等等,SpringBoot默认的Mock框架是Mockito,和junit一样,只需要依赖spring-boot-starter-test就可以了。

Mock 的中文译为仿制的,模拟的,虚假的。对于测试框架来说,即构造出一个模拟/虚假的对象,使我们的测试能顺利进行下去。

Mock 测试就是在测试过程中,对于某些 不容易构造(如 HttpServletRequest 必须在 Servlet 容器中才能构造出来)或者不容易获取 比较复杂 的对象(如 JDBC 中的 ResultSet 对象),用一个 虚拟 的对象(Mock 对象)来创建,以便测试方法。

Mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 Bean 的依赖链。

像是以下这张图,类 A 需要调用类 B 和类 C,而类 B 和类 C 又需要调用其他类如 D、E、F 等,假设类 D 是一个外部服务,那就会很难测,因为你的返回结果会直接的受外部服务影响,导致你的单元测试可能今天会过、但明天就过不了了。

而当我们引入 Mock 测试时,就可以创建一个假的对象,替换掉真实的 Bean B 和 C,这样在调用B、C的方法时,实际上就会去调用这个假的 Mock 对象的方法,而我们就可以自己设定这个 Mock 对象的参数和期望结果,让我们可以专注在测试当前的类 A,而不会受到其他的外部服务影响,这样测试效率就能提高很多。

1.2-为什么使用Mock对象?

使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。

在以下情况可以采用模拟对象来替代真实对象:

  • 真实对象的行为是不确定的(例如,当前的时间或温度);
  • 真实对象很难搭建起来;
  • 真实对象的行为很难触发(例如,网络错误);
  • 真实对象速度很慢(例如,一个完整的数据库,在测试之前可能需要初始化);
  • 真实的对象是用户界面,或包括用户界面在内;
  • 真实的对象使用了回调机制;
  • 真实对象可能还不存在;
  • 真实对象可能包含不能用作测试(而不是为实际工作)的信息和方法。

单元测试 是为了验证我们的代码运行正确性,我们注重的是代码的流程以及结果的正确与否。

对比真实运行代码,可能其中有一些 外部依赖 的构建步骤相对麻烦,如果我们还是按照真实代码的构建规则构造出外部依赖,会大大增加单元测试的工作,代码也会参杂太多非测试部分的内容,测试用例显得复杂难懂。

采用 Mock 框架,我们可以 虚拟 出一个 外部依赖,只注重代码的 流程与结果,真正地实现测试目的。

1.3-Mock测试框架的好处

  1. 可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类);
  2. 可以配置 mock 对象的行为;
  3. 可以使测试用例只注重测试流程与结果;
  4. 减少外部类、系统和依赖给单元测试带来的耦合。

1.4-Spring Boot的测试类库

现在绝大多数的java服务都是Spring框架搭建的,并且也会使用到Spring boot来进行快速搭建开发,在Spring Boot提供了许多实用工具和注解来帮助测试应用程序,主要包括以下两个模块:

  • spring-boot-test:支持测试的核心内容。
  • spring-boot-test-autoconfigure:支持测试的自动化配置。

开发进行只要使用 spring-boot-starter-test 启动器就能引入这些 Spring Boot 测试模块,还能引入一些像 JUnit, AssertJ, Hamcrest 及其他一些有用的类库,具体如下所示:

  • JUnit:Java 应用程序单元测试标准类库。
  • Spring Test & Spring Boot Test:Spring Boot 应用程序功能集成化测试支持。
  • AssertJ:一个轻量级的断言类库。
  • Mockito:一个Java Mock测试框架,默认支付 1.x,可以修改为 2.x。
  • JsonPath:一个JSON操作类库。

1.5-MockMvc

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,使得测试速度快、不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。

接口MockMvcBuilder,提供一个唯一的build方法,用来构造MockMvc。

主要有两个实现:StandaloneMockMvcBuilderDefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和集成Web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无须启动服务器)。MockMvcBuilders提供了对应的创建方法standaloneSetup方法和webAppContextSetup方法,在使用时直接调用即可。

1.6-MockMvc测试用例

1.6.1. 引入pom依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

1.6.2. MockMVC基于RESTful风格的测试

对于前后端分离的项目而言,无法直接从前端静态代码中测试接口的正确性,因此可以通过MockMVC来模拟HTTP请求。基于RESTful风格的SpringMVC的测试,我们可以测试完整的Spring MVC流程,即从URL请求到控制器处理,再到视图渲染都可以测试。

  • request

    @Data
    @ToString
    @EqualsAndHashCode
    public class WebRequest {
    private String name;

    private String mobile;
    }
  • response

    @Data
    @ToString
    @EqualsAndHashCode
    public class WebResponse<T> {
    private String code;

    private String message;

    private T body;
    }
  • 写一个简单的Controller

    @RestController
    @RequestMapping(value = "/web")
    public class WebController {

    @PostMapping(value = "/create")
    public WebResponse<String> ping(@RequestBody WebRequest webRequest){
    System.out.println(webRequest);
    WebResponse<String> response = new WebResponse<>();
    response.setBody("create 完成---");
    response.setCode("00000");
    response.setMessage("成功");
    return response;
    }
    }
  • 创建一个测试用例

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class WebControllerIT {

    @Autowired
    private WebApplicationContext mac;

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void ping() throws Exception {
    //请求的json
    String json = "{\"name\":\"王五\",\"mobile\":\"12345678901\"}";

    //perform,执行一个RequestBuilders请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
    mockMvc.perform(MockMvcRequestBuilders
    //构造一个post请求
    .post("/web/create")
    //json类型
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    //使用writeValueAsString()方法来获取对象的JSON字符串表示
    .content(json))
    //andExpect,添加ResultMathcers验证规则,验证控制器执行完成后结果是否正确,【这是一个断言】
    .andExpect(MockMvcResultMatchers.status().is(200))
    .andExpect(MockMvcResultMatchers.status().isOk())
    //使用jsonPaht验证返回的json中code字段的返回值
    .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("00000"))
    .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("成功"))
    //body属性不为空
    .andExpect(MockMvcResultMatchers.jsonPath("$.body").isNotEmpty())
    //添加ResultHandler结果处理器,比如调试时 打印结果(print方法)到控制台
    .andDo(MockMvcResultHandlers.print())
    //返回相应的MvcResult
    .andReturn();
    }

    }

1.6.3-常用API

  • 常用的期望

    //使用jsonPaht验证返回的json中code、message字段的返回值
    .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("00000"))
    .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("成功"))
    //body属性不为空
    .andExpect(MockMvcResultMatchers.jsonPath("$.body").isNotEmpty())
    // 期望的返回结果集合有2个元素 , $: 返回结果
    .andExpect(MockMvcResultMatchers.jsonPath("$.length()").value(2));
  • 常用API解释

    • RequestBuilder/MockMvcRequestBuilders

      //根据uri模板和uri变量值得到一个GET请求方式的MockHttpServletRequestBuilder;
      MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables)
      //同get类似,但是是POST方法;
      MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables)
      //同get类似,但是是PUT方法;
      MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables)
      //同get类似,但是是DELETE方法;
      MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables)
      //同get类似,但是是OPTIONS方法;
      MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables)
      //提供自己的Http请求方法及uri模板和uri变量,如上API都是委托给这个API;
      MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables)
      //提供文件上传方式的请求,得到MockMultipartHttpServletRequestBuilder;
      MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables)
      //创建一个从启动异步处理的请求的MvcResult进行异步分派的RequestBuilder;
      RequestBuilder asyncDispatch(final MvcResult mvcResult)
    • MockHttpServletRequestBuilder:

      //:添加头信息;
      MockHttpServletRequestBuilder header(String name, Object... values)/MockHttpServletRequestBuilder headers(HttpHeaders httpHeaders)
      //:指定请求的contentType头信息;
      MockHttpServletRequestBuilder contentType(MediaType mediaType)
      //:指定请求的Accept头信息;
      MockHttpServletRequestBuilder accept(MediaType... mediaTypes)/MockHttpServletRequestBuilder accept(String... mediaTypes)
      //:指定请求Body体内容;
      MockHttpServletRequestBuilder content(byte[] content)/MockHttpServletRequestBuilder content(String content)
      //:请求传入参数
      MockHttpServletRequestBuilder param(String name,String... values)
      //:指定请求的Cookie;
      MockHttpServletRequestBuilder cookie(Cookie... cookies)
      //:指定请求的Locale;
      MockHttpServletRequestBuilder locale(Locale locale)
      //:指定请求字符编码;
      MockHttpServletRequestBuilder characterEncoding(String encoding)
      //:设置请求属性数据;
      MockHttpServletRequestBuilder requestAttr(String name, Object value)
      //:设置请求session属性数据;
      MockHttpServletRequestBuilder sessionAttr(String name, Object value)/MockHttpServletRequestBuilder sessionAttrs(Map<string, object=""> sessionAttributes)
      //指定请求的flash信息,比如重定向后的属性信息;
      MockHttpServletRequestBuilder flashAttr(String name, Object value)/MockHttpServletRequestBuilder flashAttrs(Map<string, object=""> flashAttributes)
      //:指定请求的Session;
      MockHttpServletRequestBuilder session(MockHttpSession session)
      // :指定请求的Principal;
      MockHttpServletRequestBuilder principal(Principal principal)
      //:指定请求的上下文路径,必须以“/”开头,且不能以“/”结尾;
      MockHttpServletRequestBuilder contextPath(String contextPath)
      //:请求的路径信息,必须以“/”开头;
      MockHttpServletRequestBuilder pathInfo(String pathInfo)
      //:请求是否使用安全通道;
      MockHttpServletRequestBuilder secure(boolean secure)
      //:请求的后处理器,用于自定义一些请求处理的扩展点;
      MockHttpServletRequestBuilder with(RequestPostProcessor postProcessor)
    • MockMultipartHttpServletRequestBuilder

      //:指定要上传的文件;
      MockMultipartHttpServletRequestBuilder file(String name, byte[] content)/MockMultipartHttpServletRequestBuilder file(MockMultipartFile file)
    • ResultActions

      //:添加验证断言来判断执行请求后的结果是否是预期的;
      ResultActions andExpect(ResultMatcher matcher)
      //:添加结果处理器,用于对验证成功后执行的动作,如输出下请求/结果信息用于调试;
      ResultActions andDo(ResultHandler handler)
      //:返回验证成功后的MvcResult;用于自定义验证/下一步的异步处理;
      MvcResult andReturn()
    • ResultMatcher/MockMvcResultMatchers

      //:请求的Handler验证器,比如验证处理器类型/方法名;此处的Handler其实就是处理请求的控制器;
      HandlerResultMatchers handler()
      //:得到RequestResultMatchers验证器;
      RequestResultMatchers request()
      //:得到模型验证器;
      ModelResultMatchers model()
      //:得到视图验证器;
      ViewResultMatchers view()
      //:得到Flash属性验证;
      FlashAttributeResultMatchers flash()
      //:得到响应状态验证器;
      StatusResultMatchers status()
      //:得到响应Header验证器;
      HeaderResultMatchers header()
      //:得到响应Cookie验证器;
      CookieResultMatchers cookie()
      //:得到响应内容验证器;
      ContentResultMatchers content()
      //:得到Json表达式验证器;
      JsonPathResultMatchers jsonPath(String expression, Object ... args)/ResultMatcher jsonPath(String expression, Matcher matcher)
      //:得到Xpath表达式验证器;
      XpathResultMatchers xpath(String expression, Object... args)/XpathResultMatchers xpath(String expression, Map<string, string=""> namespaces, Object... args)
      //:验证处理完请求后转发的url(绝对匹配);
      ResultMatcher forwardedUrl(final String expectedUrl)
      //:验证处理完请求后转发的url(Ant风格模式匹配,@since spring4);
      ResultMatcher forwardedUrlPattern(final String urlPattern)
      //:验证处理完请求后重定向的url(绝对匹配);
      ResultMatcher redirectedUrl(final String expectedUrl)
      //:验证处理完请求后重定向的url(Ant风格模式匹配,@since spring4);
      ResultMatcher redirectedUrlPattern(final String expectedUrl)

2-Mockito

2.1-什么是Mockito?

Mockito是mocking框架,它让你用简洁的API做测试。而且Mockito简单易学,它可读性强和验证语法简洁。
Mockito是GitHub上使用最广泛的Mock框架,并与JUnit结合使用.Mockito框架可以创建和配置mock对象.使用Mockito简化了具有外部依赖的类的测试开发。

Mockito 是一种 Java Mock 框架,他主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。

像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,这就是上面提到的 Mock 测试精神,也就是通过模拟一个假的 Service 对象,来快速的测试当前我想要测试的类。

目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前内建的是 Mockito 框架。

2.2-使用模拟对象进行测试

2.2.1-单元测试的目标和挑战

单元测试应单独测试功能。如有可能,应消除其他类别或系统的副作用,以进行单元测试。

这可以通过对真正的依赖项使用测试替换(test doubles)来完成。双重测试可以分为以下几类:

  • 一个虚拟对象被传来传去,但从来没有使用过,即它的方法永远不会被调用。这样的对象可以例如用于填充方法的参数列表。
  • 对象具有有效的实现,但通常可以简化。例如,他们使用内存数据库而不是真实数据库。
  • 存根类是一个接口或类与在测试过程中使用该存根类的一个实例的目的的部分实施。存根通常不对测试中编程的内容做出任何反应。存根可能还会记录有关呼叫的信息。
  • 模拟对象是在其中定义某些方法调用的输出接口或类伪实现。模拟对象配置为在测试期间执行某些行为。他们通常记录与系统的交互,测试可以验证这一点。

可以将测试双打传递给其他要测试的对象。您的测试可以验证该类在测试过程中的正确反应。例如,您可以验证是否调用了模拟对象上的某些方法。这有助于确保您仅在运行测试时对类进行测试,并且确保测试不受任何副作用的影响。

2.2.2-模拟对象生成

您可以手动(通过代码)创建模拟对象,也可以使用模拟框架来模拟这些类。模拟框架允许您在运行时创建模拟对象并定义其行为。

模拟对象的经典示例是数据提供程序。在生产中,使用了连接到真实数据源的实现。但是对于测试,模拟对象会模拟数据源并确保测试条件始终相同。

可以将这些模拟对象提供给要测试的类。因此,要测试的类应避免对外部数据的任何硬性依赖。

模拟或模拟框架允许测试与模拟对象的预期交互。例如,您可以验证仅在模拟对象上调用了某些方法。

2.2.3-使用Mockito模拟对象

Mockito是一种流行的模拟框架,可以与JUnit结合使用。Mockito允许您创建和配置模拟对象。使用Mockito大大简化了具有外部依赖项的类的测试开发。

如果在测试中使用Mockito,通常会按照以下步骤:

  • 模拟掉外部依赖关系并将模拟插入待测代码
  • 执行被测代码
  • 验证代码是否正确执行

2.2.4-mock和stub

stub存在的意图是为了让测试对象可以正常的执行,其实现一般会硬编码一些输入和输出。

mock除了保证stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常

  • Mock和Stub都是通过一种更加快捷的方式让我们能够及时迅速的获取到我们期望的对象和数据。

  • Stub更关注于状态,它可以通过硬编码一些输入或者输出,让我们获取我们需要的数据。

  • Mock更关注于行为,它可以记录对象中各个方法都调用情况,如:是否被调用,调用了几次、在某种情况下是否会抛出异常等。

  1. stub一个mock对象的方法后,不能在同一个mock对象上再一次stub这个方法,第二次的stub无效。
  2. 不可stub一个非mock对象的方法,这种操作stub是无效的。

2.3-Mockito的使用

2.3.1-引入pom依赖

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

2.3.2-测试类中引入静态资源

import static org.mockito.Mockito.*; 

2.3.3-常用的Mockito方法

方法名描述
Mockito.mock(classToMock)模拟对象
Mockito.verify(mock)验证行为是否发生
Mockito.when(methodCall).thenReturn(value1).thenReturn(value2)触发时第一次返回value1,第n次都返回value2
Mockito.doThrow(toBeThrown).when(mock).[method]模拟抛出异常。
Mockito.mock(classToMock,defaultAnswer)使用默认Answer模拟对象
Mockito.when(methodCall).thenReturn(value)参数匹配
Mockito.doReturn(toBeReturned).when(mock).[method]参数匹配(直接执行不判断)
Mockito.when(methodCall).thenAnswer(answer))预期回调接口生成期望值
Mockito.doAnswer(answer).when(methodCall).[method]预期回调接口生成期望值(直接执行不判断)
Mockito.spy(Object)用spy监控真实对象,设置真实对象行为
Mockito.doNothing().when(mock).[method]不做任何返回
Mockito.doCallRealMethod().when(mock).[method] //等价于 Mockito.when(mock.[method]).thenCallRealMethod();调用真实的方法
reset(mock)重置mock

2.3.4-示例

1. 验证某些行为确实发生过(有没有被调用过)
// 静态导入会使代码更简洁
import static org.mockito.Mockito.*;

// mock creation 创建mock对象
List mockedList = mock(List.class);

//using mock object 使用mock对象
//验证add方法是否在前面被调用了一次,且参数为“one”。clear方法同样。
mockedList.add("one");
mockedList.clear();

//verification 验证
//下面的验证会失败。因为没有调用过add("two")。
verify(mockedList).add("one");
verify(mockedList).clear();

一旦mock对象被创建了,mock对象会记住所有的交互。然后你就可能选择性的验证你感兴趣的交互。

2. 如何做一些测试桩(Stub)

Mockito可以预设方法返回值,称为stubbing。如果这个方法没被stubbing,则默认返回null,0,false,空集合之类的。

// 你可以mock具体的类型,不仅只是接口
LinkedList mockedList = mock(LinkedList.class);

//stubbing
// 测试桩
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

// 输出“first”
System.out.println(mockedList.get(0));
// 抛出异常
System.out.println(mockedList.get(1));
// 因为get(999) 没有打桩,因此输出null
System.out.println(mockedList.get(999));

// 验证get(0)被调用的次数
verify(mockedList).get(0);
  • 插桩:使mock对象的方法返回期望值
    对于没有stub过的有返回值的方法,会返回默认值(0,false,null等)
//stubbing。当get(0)被调用时,返回"first". 方法get(1)被调用时,抛异常。
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());
  • 重复stub
//重复stub,以最后一次为准,如下将返回"second":
when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(0)).thenReturn("second");
//如下表示第一次调用时返回“first”,第二次调用时返回“second”。可以写n个。
when(mockedList.get(0)).thenReturn("first").thenReturn("second");
如果实际调用的次数超过了stub过的次数,则返回最后一次stub的值。
例如第三次调用get(0)时,则会返回"second".
  • 默认情况下,所有的函数都有返回值。mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;
  • 测试桩函数可以被覆写 : 例如常见的测试桩函数可以用于初始化夹具,但是测试函数能够覆写它。请注意,覆写测试桩函数是一种可能存在潜在问题的做法;
  • 一旦测试桩函数被调用,该函数将会一致返回固定的值;
  • 上一次调用测试桩函数有时候极为重要-当你调用一个函数很多次时,最后一次调用可能是你所感兴趣的。
3. 参数匹配器(matchers)

Mockito以自然的java风格来验证参数值: 使用equals()函数。有时,当需要额外的灵活性时你可能需要使用参数匹配器,也就是argument matchers :

Mockito.anyInt() 任何 int 值 ;
Mockito.anyLong() 任何 long 值 ;
Mockito.anyString() 任何 String 值 ;

Mockito.any(XXX.class) 任何 XXX 类型的值 等等。

 // 使用内置的anyInt()参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");
// 使用自定义的参数匹配器( 在isValid()函数中返回你自己的匹配器实现 )
when(mockedList.contains(argThat(isValid()))).thenReturn("element");
// 输出element
System.out.println(mockedList.get(999));
// 你也可以验证参数匹配器
verify(mockedList).get(anyInt());
//argument matchers can also be written as Java 8 Lambdas
verify(mockedList).add(argThat(someString -> someString.length() > 5));
// 需要注意的是,如果使用Argument matchers,那么所有参数都必须由其提供。
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher
verify(mock).someMethod(anyInt(), anyString(), "third argument");
//above is incorrect - exception will be thrown because third argument is givenwithout an argument matcher.

参数匹配器使验证和测试桩变得更灵活。点击这里查看更多内置的匹配器以及自定义参数匹配器或者hamcrest 匹配器的示例。

如果仅仅是获取自定义参数匹配器的信息,查看ArgumentMatcher类文档即可。

为了合理的使用复杂的参数匹配,使用equals()与anyX() 的匹配器会使得测试代码更简洁、简单。有时,会迫使你重构代码以使用equals()匹配或者实现equals()函数来帮助你进行测试。

同时建议你阅读第15章节或者ArgumentCaptor类文档。ArgumentCaptor是一个能够捕获参数值的特俗参数匹配器。

参数匹配器的注意点 :

如果你使用参数匹配器,所有参数都必须由匹配器提供。

示例 : ( 该示例展示了如何多次应用于测试桩函数的验证 )

verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//above is correct - eq() is also an argument matcher
// 上述代码是正确的,因为eq()也是一个参数匹配器

verify(mock).someMethod(anyInt(), anyString(), "third argument");
//above is incorrect - exception will be thrown because third argument
// 上述代码是错误的,因为所有参数必须由匹配器提供,而参数"third argument"并非由参数匹配器提供,因此的缘故会抛出异常

像anyObject(), eq()这样的匹配器函数不会返回匹配器。它们会在内部将匹配器记录到一个栈当中,并且返回一个假的值,通常为null。这样的实现是由于被Java编译器强加的静态类型安全。结果就是你不能在验证或者测试桩函数之外使用anyObject(), eq()函数。

4. 验证函数的确切、最少、从未调用次数
//using mock
mockedList.add("once");

mockedList.add("twice");
mockedList.add("twice");

mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");

//following two verifications work exactly the same - times(1) is used by default
// 下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");

//exact number of invocations verification
// 验证具体的执行次数
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");

//verification using never(). never() is an alias to times(0)
// 使用never()进行验证,never相当于times(0)
verify(mockedList, never()).add("never happened");

//verification using atLeast()/atMost()
// 使用atLeast()/atMost()
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");

verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。

5. 为返回值为void的函数通过Stub抛出异常
doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
// 调用这句代码会抛出异常
mockedList.clear();

最初,stubVoid(Object) 函数用于为无返回值的函数打桩。现在stubVoid()函数已经过时,doThrow(Throwable)成为了它的继承者。这是为了提升与 doAnswer(Answer) 函数族的可读性与一致性。

6. 验证调用执行顺序
// A. 验证mock一个对象的函数执行顺序
List singleMock = mock(List.class);

//using a single mock
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);

//using mocks
firstMock.add("was called first");
secondMock.add("was called second");

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

// 验证它们的执行顺序
inOrder.verify(firstMock).add("was called first");
inOrder.verify(secondMock).add("was called second");
// Oh, and A + B can be mixed together at will

验证执行顺序是非常灵活的-你不需要一个一个的验证所有交互,只需要验证你感兴趣的对象即可。 另外,你可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。

7. 确保交互(interaction)操作不会执行在mock对象上
// 使用Mock对象
mockOne.add("one");
// 普通验证
verify(mockOne).add("one");
// 验证某个交互是否从未被执行
verify(mockOne, never()).add("two");
// 验证mock对象没有交互过
verifyZeroInteractions(mockTwo, mockThree);
8. 查找冗余的调用
//using mocks
mockedList.add("one");
mockedList.add("two");

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

//following verification will fail
// 下面的验证将会失败
verifyNoMoreInteractions(mockedList);

一些用户可能会在频繁地使用verifyNoMoreInteractions(),甚至在每个测试函数中都用。但是verifyNoMoreInteractions()并不建议在每个测试函数中都使用。verifyNoMoreInteractions()在交互测试套件中只是一个便利的验证,它的作用是当你需要验证是否存在冗余调用时。滥用它将导致测试代码的可维护性降低。你可以阅读这篇文档来了解更多相关信息。

never()是一种更为明显且易于理解的形式。

9. 简化mock对象的创建
  • 最小化重复的创建代码
  • 使测试类的代码可读性更高
  • 使验证错误更易于阅读,因为字段名可用于标识mock对象
public class ArticleManagerTest {

@Mock private ArticleCalculator calculator;
@Mock private ArticleDatabase database;
@Mock private UserProvider userProvider;

private ArticleManager manager;

注意!下面这句代码需要在运行测试函数之前被调用,一般放到测试类的基类或者test runner中:

MockitoAnnotations.initMocks(testClass);

你可以使用内置的runner: MockitoJUnitRunner runner 或者一个rule : MockitoRule。 关于mock注解的更多信息可以阅读MockitoAnnotations文档

有2种启用Mockito注解的方法:

  • 写个@Before(JUnit4)方法,里边调用MockitoAnnotations.initMocks(this)方法。
@Before public void initMocks() {
MockitoAnnotations.initMocks(this);
}
  • 可以直接使用built-in runner: MockitoJUnitRunner。
@RunWith(MockitoJUnitRunner.class)
10. 为连续的调用做测试桩 (stub)

有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。典型的运用就是使用mock迭代器。 原始版本的Mockito并没有这个特性,例如,可以使用Iterable或者简单的集合来替换迭代器。这些方法提供了更自然的方式,在一些场景中为连续的调用做测试桩会很有用。示例如下 :

when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");

//First call: throws runtime exception:
// 第一次调用 : 抛出运行时异常
mock.someMethod("some arg");

//Second call: prints "foo"
// 第二次调用 : 输出"foo"
System.out.println(mock.someMethod("some arg"));

//Any consecutive call: prints "foo" as well (last stubbing wins).
// 后续调用 : 也是输出"foo"
System.out.println(mock.someMethod("some arg"));

另外,连续调用的另一种更简短的版本 :

// 第一次调用时返回"one",第二次返回"two",第三次返回"three"
when(mock.someMethod("some arg"))
.thenReturn("one", "two", "three");
11. 为回调做测试桩

Allows stubbing with generic Answer interface. 运行为泛型接口Answer打桩。

在最初的Mockito里也没有这个具有争议性的特性。我们建议使用thenReturn() 或thenThrow()来打桩。这两种方法足够用于测试或者测试驱动开发。

when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});

//Following prints "called with arguments: foo"
// 输出 : "called with arguments: foo"
System.out.println(mock.someMethod("foo"));
12. doReturn()、doThrow()、doAnswer()、doNothing()、doCallRealMethod()系列方法的运用

通过when(Object)为无返回值的函数打桩有不同的方法,因为编译器不喜欢void函数在括号内…

使用doThrow(Throwable) 替换stubVoid(Object)来为void函数打桩是为了与doAnswer()等函数族保持一致性。

当你想为void函数打桩时使用含有一个exception 参数的doAnswer() :

doThrow(new RuntimeException()).when(mockedList).clear();

//following throws RuntimeException:
// 下面的代码会抛出异常
mockedList.clear();

当你调用doThrow(), doAnswer(), doNothing(), doReturn() and doCallRealMethod() 这些函数时可以在适当的位置调用when()函数. 当你需要下面这些功能时这是必须的:

  • 测试void函数
  • 在受监控的对象上测试函数
  • 不知一次的测试为同一个函数,在测试过程中改变mock对象的行为。

但是在调用when()函数时你可以选择是否调用这些上述这些函数。

  • doReturn(Object toBeReturned)

    使用doReturn()在那些极少数情况下,你不能使用when(Object)

    注意when(Object)始终建议进行存根,因为它是参数类型安全的,并且更具可读性(尤其是在存续的调用存根时)。

    以下是doReturn()派上用场的罕见情况:

    1. 监视真实对象并在间谍上调用真实方法时会带来副作用

      List list = new LinkedList();
      List spy = spy(list);

      //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
      when(spy.get(0)).thenReturn("foo");

      //You have to use doReturn() for stubbing:
      doReturn("foo").when(spy).get(0);

    2. 覆盖先前的异常处理:

      when(mock.foo()).thenThrow(new RuntimeException());

      //Impossible: the exception-stubbed foo() method is called so RuntimeException is thrown.
      when(mock.foo()).thenReturn("bar");

      //You have to use doReturn() for stubbing:
      doReturn("bar").when(mock).foo();

    以上方案展示了Mockito优雅语法的折衷方案。请注意,这种情况很少见。间谍活动应该是零星的,并且覆盖异常例外非常少见。更不用说总的来说,过度存根是一种潜在的代码气味,它指出了太多的存根。

  • doThrow(Throwable… toBeThrown)

    使用doThrow()时要与存根异常的无效方法。

    when(Object)对空进行 存根需要的方法与之不同,因为编译器不喜欢方括号内的空方法…

    例:

    doThrow(new RuntimeException()).when(mock).someVoidMethod();
    • 参数:

      toBeThrown -调用存根方法时抛出

    • 返回值:

      存根-选择存根方法

  • doThrow(Class<? extends Throwable> toBeThrown)

    使用doThrow()时要与存根异常的无效方法。

    将为每个方法调用创建一个新的异常实例。

    when(Object)对空进行 存根需要的方法与之不同,因为编译器不喜欢方括号内的空方法…

    例:

    doThrow(RuntimeException.class).when(mock).someVoidMethod();
    • 参数:

      toBeThrown -调用存根方法时抛出

    • 返回值:

      存根-选择存根方法

  • doAnswer(Answer answer)

    doAnswer()当您想用generic对无效方法进行存根时使用Answer

    when(Object)对空进行 存根需要的方法与之不同,因为编译器不喜欢方括号内的空方法…

    例:

    doAnswer(new Answer() {
    public Object answer(InvocationOnMock invocation) {
    Object[] args = invocation.getArguments();
    Mock mock = invocation.getMock();
    return null;
    }})
    .when(mock).someMethod();

    请参阅Javadoc中的Mockito类示例

    • 参数:

      answer -在调用存根方法时回答

    • 返回值:

      存根-选择存根方法

  • doNothing()

    使用doNothing()设置无效的方法什么也不做。注意,默认情况下,模拟上的void方法什么也不做! 但是,在少数情况下,doNothing()会派上用场:

    1. 取消对void方法的连续调用:

      doNothing().
      doThrow(new RuntimeException())
      .when(mock).someVoidMethod();

      //does nothing the first time:
      mock.someVoidMethod();

      //throws RuntimeException the next time:
      mock.someVoidMethod();
    2. 当您监视真实对象并且您希望void方法不执行任何操作时:

      List list = new LinkedList();
      List spy = spy(list);

      //let's make clear() do nothing
      doNothing().when(spy).clear();

      spy.add("one");

      //clear() does nothing, so the list still contains "one"
      spy.clear();
  • doCallRealMethod()

    使用doCallRealMethod()时要调用真正执行的方法。

    像往常一样,您将阅读部分模拟警告:面向对象的编程通过将复杂度划分为单独的,特定的SRPy对象来解决复杂度问题。部分模拟如何适应这种范例?好吧,事实并非如此……部分模拟通常意味着复杂性已移至同一对象的不同方法。在大多数情况下,这不是您设计应用程序的方式。

    但是,在少数情况下,局部模拟会派上用场:处理您无法轻松更改的代码(第三方接口,遗留代码的临时重构等)。但是,我不会将局部模拟用于新的,测试驱动的以及设计的代码。

    另请参见javadoc spy(Object)以了解有关部分模拟的更多信息。 Mockito.spy()是创建部分模拟的推荐方法。 原因是它保证针对正确构造的对象调用真实方法,因为您负责构造传递给spy()方法的对象。

    例:

    Foo mock = mock(Foo.class);
    doCallRealMethod().when(mock).someVoidMethod();

    // this will call the real implementation of Foo.someVoidMethod()
    mock.someVoidMethod();

    请参阅Javadoc中的Mockito类示例

    • 返回值:

      存根-选择存根方法

13. 监控真实对象

你可以为真实对象创建一个监控(spy)对象。当你使用这个spy对象时真实的对象也会也调用,除非它的函数被stub了。尽量少使用spy对象,使用时也需要小心形式,例如spy对象可以用来处理遗留代码。

监控一个真实的对象可以与“局部mock对象”概念结合起来。在1.8之前,mockito的监控功能并不是真正的局部mock对象。原因是我们认为局部mock对象的实现方式并不好,在某些时候我发现一些使用局部mock对象的合法用例。(第三方接口、临时重构遗留代码,完整的文章在这里

List list = new LinkedList();
List spy = spy(list);

// 你可以为某些函数打桩
when(spy.size()).thenReturn(100);
// 通过spy对象调用真实对象的函数
spy.add("one");
spy.add("two");

// 输出第一个元素
System.out.println(spy.get(0));
// 因为size()函数被打桩了,因此这里返回的是100
System.out.println(spy.size());

// 交互验证
verify(spy).add("one");
verify(spy).add("two");

理解监控真实对象非常重要!

有时,在监控对象上使用when(Object)来进行打桩是不可能或者不切实际的。因此,当使用监控对象时请考虑doReturn|Answer|Throw()函数族来进行打桩。例如 :

List list = new LinkedList();
List spy = spy(list);

// 不可能 : 因为当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生IndexOutOfBoundsException异常,因为真实List对象是空的
when(spy.get(0)).thenReturn("foo");

// 你需要使用doReturn()来打桩
doReturn("foo").when(spy).get(0);

Mockito并不会为真实对象代理函数调用,实际上它会拷贝真实对象。因此如果你保留了真实对象并且与之交互,不要期望从监控对象得到正确的结果。当你在监控对象上调用一个没有被stub的函数时并不会调用真实对象的对应函数,你不会在真实对象上看到任何效果。

因此结论就是 : 当你在监控一个真实对象时,你想在stub这个真实对象的函数,那么就是在自找麻烦。或者你根本不应该验证这些函数。

14. 修改没有测试桩的调用的默认返回值

你可以指定策略来创建mock对象的返回值。这是一个高级特性,通常来说,你不需要写这样的测试。然后,它对于遗留系统来说是很有用处的。当你不需要为函数调用打桩时你可以指定一个默认的answer。

Foo mock = mock(Foo.class, Mockito.RETURNS_SMART_NULLS);
Foo mockTwo = mock(Foo.class, new YourOwnAnswer());

关于RETURNS_SMART_NULLS更多的信息请查看 : RETURNS_SMART_NULLS文档

15. 为下一步的断言捕获参数

Mockito以java代码风格的形式来验证参数值 : 即通过使用equals()函数。这也是我们推荐用于参数匹配的方式,因为这样会使得测试代码更简单、简洁。在某些情况下,当验证交互之后要检测真实的参数值时这将变得有用。例如 :

ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
// 参数捕获
verify(mock).doSomething(argument.capture());
// 使用equal断言
assertEquals("John", argument.getValue().getName());

警告 : 我们建议使用没有测试桩的ArgumentCaptor来验证,因为使用含有测试桩的ArgumentCaptor会降低测试代码的可读性,因为captor是在断言代码块之外创建的。另一个好处是它可以降低本地化的缺点,因为如果测试桩函数没有被调用,那么参数就不会被捕获。总之,ArgumentCaptor与自定义的参数匹配器相关(可以查看ArgumentMatcher类的文档 )。这两种技术都能用于检测外部传递到Mock对象的参数。然而,使用ArgumentCaptor在以下的情况下更合适 :

  • 自定义不能被重用的参数匹配器
  • 你仅需要断言参数值

自定义参数匹配器相关的资料你可以参考ArgumentMatcher文档。

更多示例和API请看官方文档——Mockito文档

2.3.5-使用Mockito

1. 模拟抛出异常
@Test(expected = IOException.class)//期望报IO异常
public void when_thenThrow() throws IOException{
OutputStream mock = Mockito.mock(OutputStream.class);
//预设当流关闭时抛出异常
Mockito.doThrow(new IOException()).when(mock).close();
mock.close();
}
2. 使用默认Answer模拟对象

RETURNS_DEEP_STUBS 是创建mock对象时的备选参数之一

@Test
public void Test(){
A a=Mockito.mock(A.class,Mockito.RETURNS_DEEP_STUBS);
Mockito.when(a.getB().getName()).thenReturn("Beijing");
Assert.assertEquals("Beijing",a.getB().getName());
}

@Test
public void Test2(){
A a=Mockito.mock(A.class);
B b=Mockito.mock(B.class);
Mockito.when(a.getB()).thenReturn(b);
Mockito.when(b.getName()).thenReturn("Beijing");
Assert.assertEquals("Beijing",a.getB().getName());
}
class A{
private B b;
public B getB(){
return b;
}
public void setB(B b){
this.b=b;
}
}
class B{
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getSex(Integer sex){
if(sex==1){
return "man";
}else{
return "woman";
}
}
}
3. 参数匹配
@Test
public void with_arguments(){
B b = Mockito.mock(B.class);
//预设根据不同的参数返回不同的结果
Mockito.when(b.getSex(1)).thenReturn("男");
Mockito.when(b.getSex(2)).thenReturn("女");
Assert.assertEquals("男", b.getSex(1));
Assert.assertEquals("女", b.getSex(2));
//对于没有预设的情况会返回默认值
Assert.assertEquals(null, b.getSex(0));
}
class B{
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
public String getSex(Integer sex){
if(sex==1){
return "man";
}else{
return "woman";
}
}
}
4. 匹配任意参数

Mockito.anyInt() 任何 int 值 ;
Mockito.anyLong() 任何 long 值 ;
Mockito.anyString() 任何 String 值 ;

Mockito.any(XXX.class) 任何 XXX 类型的值 等等。

@Test
public void with_unspecified_arguments(){
List list = Mockito.mock(List.class);
//匹配任意参数
Mockito.when(list.get(Mockito.anyInt())).thenReturn(1);
Mockito.when(list.contains(Mockito.argThat(new IsValid()))).thenReturn(true);
Assert.assertEquals(1,list.get(1));
Assert.assertEquals(1,list.get(999));
Assert.assertTrue(list.contains(1));
Assert.assertTrue(!list.contains(3));
}
class IsValid extends ArgumentMatcher<List>{
@Override
public boolean matches(Object obj) {
return obj.equals(1) || obj.equals(2);
}
}

注意:使用了参数匹配,那么所有的参数都必须通过matchers来匹配
Mockito继承Matchers,anyInt()等均为Matchers方法
当传入两个参数,其中一个参数采用任意参数时,指定参数需要matchers来对比

Comparator comparator = mock(Comparator.class);
comparator.compare("nihao","hello");
//如果你使用了参数匹配,那么所有的参数都必须通过matchers来匹配
Mockito.verify(comparator).compare(Mockito.anyString(),Mockito.eq("hello"));
//下面的为无效的参数匹配使用
//verify(comparator).compare(anyString(),"hello");
5. 自定义参数匹配
@Test
public void argumentMatchersTest(){
//创建mock对象
List<String> mock = mock(List.class);
//argThat(Matches<T> matcher)方法用来应用自定义的规则,可以传入任何实现Matcher接口的实现类。
Mockito.when(mock.addAll(Mockito.argThat(new IsListofTwoElements()))).thenReturn(true);
Assert.assertTrue(mock.addAll(Arrays.asList("one","two","three")));
}

class IsListofTwoElements extends ArgumentMatcher<List>
{
public boolean matches(Object list)
{
return((List)list).size()==3;
}
}
6. 预期回调接口生成期望值
@Test
public void answerTest(){
List mockList = Mockito.mock(List.class);
//使用方法预期回调接口生成期望值(Answer结构)
Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new CustomAnswer());
Assert.assertEquals("hello world:0",mockList.get(0));
Assert.assertEquals("hello world:999",mockList.get(999));
}
private class CustomAnswer implements Answer<String> {
@Override
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return "hello world:"+args[0];
}
}
等价于:(也可使用匿名内部类实现)
@Test
public void answer_with_callback(){
//使用Answer来生成我们我们期望的返回
Mockito.when(mockList.get(Mockito.anyInt())).thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return "hello world:"+args[0];
}
});
Assert.assertEquals("hello world:0",mockList.get(0));
Assert. assertEquals("hello world:999",mockList.get(999));
}
7. 预期回调接口生成期望值(直接执行)
@Test
public void testAnswer1(){
List<String> mock = Mockito.mock(List.class);
Mockito.doAnswer(new CustomAnswer()).when(mock).get(Mockito.anyInt());
Assert.assertEquals("大于三", mock.get(4));
Assert.assertEquals("小于三", mock.get(2));
}
public class CustomAnswer implements Answer<String> {
public String answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
Integer num = (Integer)args[0];
if( num>3 ){
return "大于三";
} else {
return "小于三";
}
}
}
8. 修改对未预设的调用返回默认期望(指定返回值)
//mock对象使用Answer来对未预设的调用返回默认期望值
List mock = Mockito.mock(List.class,new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
return 999;
}
});
//下面的get(1)没有预设,通常情况下会返回NULL,但是使用了Answer改变了默认期望值
Assert.assertEquals(999, mock.get(1));
//下面的size()没有预设,通常情况下会返回0,但是使用了Answer改变了默认期望值
Assert.assertEquals(999,mock.size());
9. 用spy监控真实对象,设置真实对象行为
@Test(expected = IndexOutOfBoundsException.class)
public void spy_on_real_objects(){
List list = new LinkedList();
List spy = Mockito.spy(list);
//下面预设的spy.get(0)会报错,因为会调用真实对象的get(0),所以会抛出越界异常
//Mockito.when(spy.get(0)).thenReturn(3);

//使用doReturn-when可以避免when-thenReturn调用真实对象api
Mockito.doReturn(999).when(spy).get(999);
//预设size()期望值
Mockito.when(spy.size()).thenReturn(100);
//调用真实对象的api
spy.add(1);
spy.add(2);
Assert.assertEquals(100,spy.size());
Assert.assertEquals(1,spy.get(0));
Assert.assertEquals(2,spy.get(1));
Assert.assertEquals(999,spy.get(999));
}
10. 不做任何返回
@Test
public void Test() {
A a = Mockito.mock(A.class);
//void 方法才能调用doNothing()
Mockito.doNothing().when(a).setName(Mockito.anyString());
a.setName("bb");
Assert.assertEquals("bb",a.getName());
}
class A {
private String name;
private void setName(String name){
this.name = name;
}
private String getName(){
return name;
}
}
11. 调用真实的方法
@Test
public void Test() {
A a = Mockito.mock(A.class);
//void 方法才能调用doNothing()
Mockito.when(a.getName()).thenReturn("bb");
Assert.assertEquals("bb",a.getName());
//等价于Mockito.when(a.getName()).thenCallRealMethod();
Mockito.doCallRealMethod().when(a).getName();
Assert.assertEquals("zhangsan",a.getName());
}
class A {
public String getName(){
return "zhangsan";
}
}
12. 重置 mock
@Test
public void reset_mock(){
List list = mock(List.class);
Mockito. when(list.size()).thenReturn(10);
list.add(1);
Assert.assertEquals(10,list.size());
//重置mock,清除所有的互动和预设
Mockito.reset(list);
Assert.assertEquals(0,list.size());
}
13. @Mock 注解
public class MockitoTest {
@Mock
private List mockList;
//必须在基类中添加初始化mock的代码,否则报错mock的对象为NULL
public MockitoTest(){
MockitoAnnotations.initMocks(this);
}
@Test
public void AnnoTest() {
mockList.add(1);
Mockito.verify(mockList).add(1);
}
}
14. 指定测试类使用运行器:MockitoJUnitRunner
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest2 {
@Mock
private List mockList;

@Test
public void shorthand(){
mockList.add(1);
Mockito.verify(mockList).add(1);
}
}

2.3.6-@MockBean

使用 @MockBean 可以解决单元测试中的一些依赖问题,示例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceWithMockBeanTest {
@MockBean
SampleDependencyA dependencyA;
@Autowired
SampleService sampleService;

@Test
public void testDependency() {
when(dependencyA.getExternalValue(anyString())).thenReturn("mock val: A");
assertEquals("mock val: A", sampleService.foo());
}
}

@MockBean 只能 mock 本地的代码——或者说是自己写的代码,对于储存在库中而且又是以 Bean 的形式装配到代码中的类无能为力。

@SpyBean 解决了 SpringBoot 的单元测试中 @MockBean 不能 mock 库中自动装配的 Bean 的局限

参考自:

https://www.jianshu.com/p/ecbd7b5a2021

https://www.imooc.com/article/292626

2.3.8-在 SpringBoot 单元测试中使用 Mockito

首先在 pom.xml 下新增 spring-boot-starter-test 依赖,该依赖内就有包含了 JUnit、Mockito。

<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

先写好一个 UserService,他里面有两个方法 getUserById() 和 insertUser(),而他们会分别去再去调用 UserDao 这个 bean的 getUserById() 和 insertUser() 方法。

@Component
public class UserService {
@Autowired
private UserDao userDao;

public User getUserById(Integer id) {
return userDao.getUserById(id);
}

public Integer insertUser(User user) {
return userDao.insertUser(user);
}
}

User Model 的定义如下:

@Data
public class User {
private Integer id;
private String name;
}

如果这时候我们先不使用 Mockito 模拟一个假的 userDao Bean,而是真的去调用一个正常的 Spring Bean 的 userDao 的话,测试类写法如下。其实就是很普通的注入 userService Bean,然后去调用他的方法,而他会再去调用 userDao 取得数据库的数据,然后我们再对返回结果做 Assert 断言检查。

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
//先普通的注入一个userService bean
@Autowired
private UserService userService;
@Test
public void getUserById() throws Exception {
//普通的使用userService,他里面会再去调用userDao取得数据库的数据
User user = userService.getUserById(1);
//检查结果
Assert.assertNotNull(user);
Assert.assertEquals(user.getId(), new Integer(1));
Assert.assertEquals(user.getName(), "John");
}
}

但是如果 userDao 还没写好,又想先测 userService 的话,就需要使用 Mockito 去模拟一个假的 userDao 出来。

使用方法是在 userDao 上加上一个 @MockBean 注解,当 userDao 被加上这个注解之后,表示 Mockito 会帮我们创建一个假的 Mock 对象,替换掉 Spring 中已存在的那个真实的 userDao Bean,也就是说,注入进 userService 的 userDao Bean,已经被我们替换成假的 Mock 对象了,所以当我们再次调用 userService 的方法时,会去调用的实际上是 mock userDao Bean 的方法,而不是真实的 userDao Bean。

当我们创建了一个假的 userDao 后,我们需要为这个 mock userDao 自定义方法的返回值,这里有一个公式用法,下面这段代码的意思为,当调用了某个 Mock 对象的方法时,就回传我们想要的自定义结果。

Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 )

使用 Mockito 模拟 Bean 的单元测试具体实例如下:

@RunWith(SpringRunner.class)
@SpringBootTest
publicclass UserServiceTest {
@Autowired
private UserService userService;
@MockBean
private UserDao userDao;
@Test
public void getUserById() throws Exception {
// 定义当调用mock userDao的getUserById()方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
Mockito.when(userDao.getUserById(3)).thenReturn(new User(200, "I'm mock 3")); // 返回的会是名字为I'm mock 3的user对象
User user = userService.getUserById(1);
Assert.assertNotNull(user);
Assert.assertEquals(user.getId(), new Integer(200));
Assert.assertEquals(user.getName(), "I'm mock 3");
}
}

Mockito 除了最基本的 Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ),还提供了其他用法让我们使用。

1. thenReturn 系列方法

当使用任何整数值调用 userService 的 getUserById() 方法时,就回传一个名字为 I’m mock3 的 User 对象。

Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById(200); // 回传的user的名字也为I'm mock

限制只有当参数的数字是 3 时,才会回传名字为 I’m mock 3 的 user 对象。

Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "I'm mock"));
User user1 = userService.getUserById(3); // 回传的user的名字为I'm mock
User user2 = userService.getUserById(200); // 回传的user为null

当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什么,都回传 100。

Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
Integer i = userService.insertUser(new User()); //会返回100
  1. thenThrow 系列方法

当调用 userService 的 getUserById() 时的参数是 9 时,抛出一个 RuntimeException。

Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
User user = userService.getUserById(9); //会抛出一个RuntimeException

如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception。

Mockito.doThrow(new RuntimeException("mock throw exception")).when(userService).print();
userService.print(); //会抛出一个RuntimeException
3. verify 系列方法

检查调用 userService 的 getUserById()、且参数为3的次数是否为1次。

Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ;

验证调用顺序,验证 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser() 方法。

InOrder inOrder = Mockito.inOrder(userService);
inOrder.verify(userService).getUserById(3);
inOrder.verify(userService).getUserById(5);
inOrder.verify(userService).insertUser(Mockito.any(User.class));

2.3.8-在项目中使用Mockito

  • 创建一个service

    public interface WebService {

    String web(String string);
    }
    @Service
    public class WebServiceImpl implements WebService {

    @Override
    public String web(String string) {
    return "WebServiceImpl 运行成功";
    }
    }
  • 创建Controller

    @RestController
    @RequestMapping(value = "/web")
    public class WebController {

    @Autowired
    private WebService webService;

    @PostMapping(value = "/create")
    public WebResponse<String> ping(@RequestBody WebRequest webRequest){

    //调用service
    String str = webService.web(webRequest.getMobile());

    WebResponse<String> response = new WebResponse<>();
    response.setBody(str);
    response.setCode("00000");
    response.setMessage("成功");
    return response;
    }
    }
  • 创建测试用例

    @RunWith(SpringRunner.class)
    @SpringBootTest
    @AutoConfigureMockMvc
    public class WebControllerIT {

    @Autowired
    private WebApplicationContext mac;

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private WebService webService;

    @Test
    public void ping() throws Exception {

    doReturn("Mockito WebServiceImpl 运行完成").when(webService).web(anyString());

    //请求的json
    String json = "{\"name\":\"王五\",\"mobile\":\"12345678901\"}";

    //perform,执行一个RequestBuilders请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理
    mockMvc.perform(MockMvcRequestBuilders
    //构造一个post请求
    .post("/web/create")
    //json类型
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    //使用writeValueAsString()方法来获取对象的JSON字符串表示
    .content(json))
    //andExpect,添加ResultMathcers验证规则,验证控制器执行完成后结果是否正确,【这是一个断言】
    .andExpect(MockMvcResultMatchers.status().is(200))
    .andExpect(MockMvcResultMatchers.status().isOk())
    //使用jsonPaht验证返回的json中code字段的返回值
    .andExpect(MockMvcResultMatchers.jsonPath("$.code").value("00000"))
    .andExpect(MockMvcResultMatchers.jsonPath("$.message").value("成功"))
    //body属性不为空
    .andExpect(MockMvcResultMatchers.jsonPath("$.body").isNotEmpty())
    //添加ResultHandler结果处理器,比如调试时 打印结果(print方法)到控制台
    .andDo(MockMvcResultHandlers.print())
    //返回相应的MvcResult
    .andReturn();
    }

    }

    从上面的代码可以看到,我们新增了一个webService,并增加了@MockBean注解,表示将webService给mock调,这样我们就可以增加自己想要得webService返回结果。
    在测试用例中我们增加了doReturn()方法,这段代码的含义是当调用WebService中的web()方法时(anyString()表示传入web()方法中的参数是任意的String类型,当然还有个anyInt()等方法),返回Mockito WebServiceImpl 运行完成。当然你也可以不将WebService给mock掉,这样拿到的就是正常的返回值。

2.3.9-Mockito的限制

上述就是 Mockito 的 Mock 对象使用方法,不过当使用 Mockito 在 Mock 对象时,有一些限制需要遵守:

  • 不能 Mock 静态方法
  • 不能 Mock private 方法
  • 不能 Mock final class

Mockito不能实现对对静态函数、构造函数、私有函数、Final 函数以及系统函数的模拟。

因此在写代码时,需要做良好的功能拆分,才能够使用 Mockito 的 Mock 技术,帮助我们降低测试时 Bean 的耦合度。

2.4-总结

Mockito 是一个非常强大的框架,可以在执行单元测试时帮助我们模拟一个 Bean,提高单元测试的稳定性。

并且大家可以尝试在写代码时,从 Mock 测试的角度来写,更能够写出功能切分良好的代码架构,像是如果有把专门和外部服务沟通的代码抽出来成一个 Bean,在进行单元测试时,只要透过 Mockito 更换掉那个 Bean 就行了。

3-EasyMock

3.1-EasyMock简介

手动的构造 Mock 对象会给开发人员带来额外的编码量,而且这些为创建 Mock 对象而编写的代码很有可能引入错误。目前,有许多开源项目对动态构建 Mock 对象提供了支持,这些项目能够根据现有的接口或类动态生成,这样不仅能避免额外的编码工作,同时也降低了引入错误的可能。

EasyMock 是一套用于通过简单的方法对于给定的接口生成 Mock 对象的类库。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,我们可以方便的构造 Mock 对象从而使单元测试顺利进行。

EasyMock官方文档:http://easymock.org/user-guide.html

3.2-EasyMock的使用

3.2.1-添加pom依赖

EasyMock在Maven中央存储库中可用。只需将以下依赖项添加到您的pom.xml中:

<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>4.2</version>
<scope>test</scope>
</dependency>

3.2.2-被测试的对象

public class UserService{
@Resource
private UserDao userDao;

private String getUserName(String userId)
{
User user = userDao.select(userId);

if (null == user)
{
return "";
}else {
return "offline_" + user.getName();
}
}
}

其中UserService依赖UserDao接口的实现

3.2.3-测试代码

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:CommonUtils.xml")
public class UserServiceTest{

private UserDao userDaoMock = null;

private User userMock = null;

@Resource
private UserService userService;

@Before
public void setUp()
{
// 创建mock对象
userDaoMock = EasyMock.createMock(UserDao.class);
userMock = EasyMock.createMock(User.class);

// mock对象注入被测对象
Field userDaoField = userService.getDeclaredField("userDao");
userDaoField.setAccessible(true);
userDaoField.set(userService,userDaoMock);
}


@Test
public void getUserName()
{
// 设置mock对象的期望值
EasyMock.expect(userMock.getName()).andReturn("testName");
EasyMock.expect(userDaoMock.select("1")).andReturn(userMock);

// mock对象的replay
EasyMock.replay(userMock);
EasyMock.replay(userDaoMock);

// 断言
Assert.assertTrue(userService.getUserName("1").equals("offline_testName"));

// 验证mock对象是否被调用
EasyMock.verify(userMock);
EasyMock.verify(userDaoMock);
}

@After
public void tearDown() throws Exception {
userDaoMock = null;
userMock = null;
}
}

根据官方文档,mock的todolist有:

  1. 创建模拟
  2. 将其设置为经过测试的课程
  3. 记录我们期望的模拟操作
  4. 告诉所有模拟我们正在进行实际测试
  5. 测试
  6. 确保应调用的所有内容均已调用

我们范例中对应是:

1.使用EasyMock生成Mock对象:

userDaoMock = EasyMock.createMock(UserDao.class);
userMock = EasyMock.createMock(User.class);

2.Mock对象注入被测对象:

Field userDaoField = userService.getDeclaredField("userDao");
userDaoField.setAccessible(true);
userDaoField.set(userService,userDaoMock);

3.设定Mock对象的预期行为和输出:

EasyMock.expect(userMock.getName()).andReturn("testName");
EasyMock.expect(userDaoMock.select("1")).andReturn(userMock);

4.将Mock对象切换到Replay状态:

EasyMock.replay(userMock);
EasyMock.replay(userDaoMock);

5.调用Mock对象方法进行单元测试:

Assert.assertTrue(userService.getUserName("1").equals("offline_testName"));

6.确定mock对象有被调用:

EasyMock.verify(userMock);
EasyMock.verify(userDaoMock);

3.3-EasyMock进阶

1. 使用注解

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:CommonUtils.xml")
public class UserServiceTest{

@Rule
private EasyMockRule mocks = new EasyMockRule(this);

private IMocksControl iMocksControl = null;

@Mock
private UserDao userDaoMock = null;

private User userMock = null;

@TestSubject
@Resource
private UserService userService;
...
}

解析:

1.mock对象会被注入到userService对象中,@Mock是需要创建的mock对象,@TestSubject是被测类

2.如果没有下面这段代码设置Junit的rule,需要将测试类的Runner设置为@RunWith(EasyMockRunner.class),当你希望使用别的runner类,但是又需要EasyMock的注解时,使用Junit的rule进行扩展

``@Ruleprivate EasyMockRule mocks = new EasyMockRule(this);

2. 使用IMocksControl管理多个mock对象

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:CommonUtils.xml")
public class UserServiceTest{

private IMocksControl iMocksControl = null;

private UserDao userDaoMock = null;

private User userMock = null;

@Resource
private UserService userService;

@Before
public void setUp()
{
// 创建mock对象
iMocksControl = EasyMock.createControl();
userDaoMock = iMocksControl.createMock(UserDao.class);
userMock = iMocksControl.createMock(User.class);

// mock对象注入被测对象
Field userDaoField = userService.getDeclaredField("userDao");
userDaoField.setAccessible(true);
userDaoField.set(userService,userDaoMock);
}


@Test
public void getUserName()
{
// 设置mock对象的期望值
EasyMock.expect(userMock.getName()).andReturn("testName");
EasyMock.expect(userDaoMock.select("1")).andReturn(userMock);

// mock对象的replay
iMocksControl.replay();

// 断言
Assert.assertTrue(userService.getUserName("1").equals("offline_testName"));

// 验证mock对象是否被调用
iMocksControl.verify();
}

@After
public void tearDown() throws Exception {
userDaoMock = null;
userMock = null;
}
}

3. 设定 Mock 对象的预期行为和输出

使用 EasyMock 动态构建 ResultSet 接口的 Mock 对象

ResultSet mockResultSet = createMock(ResultSet.class);

其中 createMockorg.easymock.EasyMock 类所提供的静态方法,你可以通过 static import 将其引入。

如果需要在相对复杂的测试用例中使用多个 Mock 对象,EasyMock 提供了另外一种生成和管理 Mock 对象的机制:

IMocksControl control = EasyMock.createControl();
java.sql.Connection mockConnection = control.createMock(Connection.class);
java.sql.Statement mockStatement = control.createMock(Statement.class);
java.sql.ResultSet mockResultSet = control.createMock(ResultSet.class);

EasyMock 类的 createControl 方法能创建一个接口 IMocksControl 的对象,该对象能创建并管理多个 Mock 对象。如果需要在测试中使用多个 Mock 对象,我们推荐您使用这一机制,因为它在多个 Mock 对象的管理上提供了相对便捷的方法。

在一个完整的测试过程中,一个 Mock 对象将会经历两个状态:Record 状态和 Replay 状态。Mock 对象一经创建,它的状态就被置为 Record。在 Record 状态,用户可以设定 Mock 对象的预期行为和输出,这些对象行为被录制下来,保存在 Mock 对象中。

添加 Mock 对象行为的过程通常可以分为以下3步:

  • 对 Mock 对象的特定方法作出调用;
  • 通过 org.easymock.EasyMock 提供的静态方法 expectLastCall 获取上一次方法调用所对应的 IExpectationSetters 实例;
  • 通过 IExpectationSetters 实例设定 Mock 对象的预期输出。

设定预期返回值

Mock 对象的行为可以简单的理解为 Mock 对象方法的调用和方法调用所产生的输出。在 EasyMock 2.3 中,对 Mock 对象行为的添加和设置是通过接口 IExpectationSetters 来实现的。Mock 对象方法的调用可能产生两种类型的输出:

  • 产生返回值;

  • 抛出异常。

接口 IExpectationSetters 提供了多种设定预期输出的方法,其中和设定返回值相对应的是 andReturn 方法:

IExpectationSetters<T> andReturn(T value);

我们仍然用 ResultSet 接口的 Mock 对象为例,如果希望方法 mockResult.getString(1) 的返回值为 “My return value”,那么你可以使用以下的语句:

mockResultSet.getString(1);
expectLastCall().andReturn("My return value");

以上的语句表示 mockResultSetgetString 方法被调用一次,这次调用的返回值是 “My return value”。有时,我们希望某个方法的调用总是返回一个相同的值,为了避免每次调用都为 Mock 对象的行为进行一次设定,我们可以用设置默认返回值的方法:

void andStubReturn(Object value);

假设我们创建了 StatementResultSet 接口的 Mock 对象 mockStatement 和 mockResultSet,在测试过程中,我们希望 mockStatement 对象的 executeQuery 方法总是返回 mockResultSet,我们可以使用如下的语句

mockStatement.executeQuery("SELECT * FROM sales_order_table");
expectLastCall().andStubReturn(mockResultSet);

EasyMock 在对参数值进行匹配时,默认采用 Object.equals() 方法。因此,如果我们以 "select * from sales_order_table" 作为参数,预期方法将不会被调用。如果您希望上例中的 SQL 语句能不区分大小写,可以用特殊的参数匹配器来解决这个问题,我们将在 “在 EasyMock 中使用参数匹配器” 一章对此进行说明。

设定预期异常抛出

对象行为的预期输出除了可能是返回值外,还有可能是抛出异常。IExpectationSetters 提供了设定预期抛出异常的方法:

IExpectationSetters<``T``> andThrow(Throwable throwable);

和设定默认返回值类似,IExpectationSetters 接口也提供了设定抛出默认异常的函数:

void andStubThrow(Throwable throwable);

设定预期方法调用次数

通过以上的函数,您可以对 Mock 对象特定行为的预期输出进行设定。除了对预期输出进行设定,IExpectationSetters 接口还允许用户对方法的调用次数作出限制。在 IExpectationSetters 所提供的这一类方法中,常用的一种是 times 方法:

IExpectationSetters<T>times(int count);

该方法可以 Mock 对象方法的调用次数进行确切的设定。假设我们希望 mockResultSet 的 getString 方法在测试过程中被调用3次,期间的返回值都是 “My return value”,我们可以用如下语句:

mockResultSet.getString(1);
expectLastCall().andReturn("My return value").times(3);

注意到 andReturnandThrow 方法的返回值依然是一个 IExpectationSetters 实例,因此我们可以在此基础上继续调用 times 方法。

除了设定确定的调用次数,IExpectationSetters 还提供了另外几种设定非准确调用次数的方法:
times(int minTimes, int maxTimes):该方法最少被调用 minTimes 次,最多被调用 maxTimes 次。
atLeastOnce():该方法至少被调用一次。
anyTimes():该方法可以被调用任意次。

某些方法的返回值类型是 void,对于这一类方法,我们无需设定返回值,只要设置调用次数就可以了。以 ResultSet 接口的 close 方法为例,假设在测试过程中,该方法被调用3至5次:

mockResultSet.close();``expectLastCall().times(3, 5);

为了简化书写,EasyMock 还提供了另一种设定 Mock 对象行为的语句模式。对于上例,您还可以将它写成:

expect(mockResult.close()).times(3, 5);

这个语句和上例中的语句功能是完全相同的。

4. 将 Mock 对象切换到 Replay 状态

在生成 Mock 对象和设定 Mock 对象行为两个阶段,Mock 对象的状态都是 Record 。在这个阶段,Mock 对象会记录用户对预期行为和输出的设定。

在使用 Mock 对象进行实际的测试前,我们需要将 Mock 对象的状态切换为 Replay。在 Replay 状态,Mock 对象能够根据设定对特定的方法调用作出预期的响应。将 Mock 对象切换成 Replay 状态有两种方式,您需要根据 Mock 对象的生成方式进行选择。如果 Mock 对象是通过 org.easymock.EasyMock 类提供的静态方法 createMock 生成的(第1节中介绍的第一种 Mock 对象生成方法),那么 EasyMock 类提供了相应的 replay 方法用于将 Mock 对象切换为 Replay 状态:

replay(mockResultSet);

如果 Mock 对象是通过 IMocksControl 接口提供的 createMock 方法生成的(第1节中介绍的第二种Mock对象生成方法),那么您依旧可以通过 IMocksControl 接口对它所创建的所有 Mock 对象进行切换:

control.replay();

以上的语句能将在第3节中生成的 mockConnection、mockStatement 和 mockResultSet 等3个 Mock 对象都切换成 Replay 状态。

5. 对 Mock 对象的行为进行验证

在利用 Mock 对象进行实际的测试过程之后,我们还有一件事情没有做:对 Mock 对象的方法调用的次数进行验证。

为了验证指定的方法调用真的完成了,我们需要调用 verify 方法进行验证。和 replay 方法类似,您需要根据 Mock 对象的生成方式来选用不同的验证方式。如果 Mock 对象是由 org.easymock.EasyMock 类提供的 createMock 静态方法生成的,那么我们同样采用 EasyMock 类的静态方法 verify 进行验证:

verify(mockResultSet);

如果Mock对象是有 IMocksControl 接口所提供的 createMock 方法生成的,那么采用该接口提供的 verify 方法,例如第3节中的 IMocksControl 实例 control:

control.verify();

6. Mock 对象的重用

为了避免生成过多的 Mock 对象,EasyMock 允许对原有 Mock 对象进行重用。要对 Mock 对象重新初始化,我们可以采用 reset 方法。和 replay 和 verify 方法类似,EasyMock 提供了两种 reset 方式:

  • 如果 Mock 对象是由 org.easymock.EasyMock 类中的静态方法 createMock 生成的,那么该 Mock 对象的可以用 EasyMock 类的静态方法 reset 重新初始化;

  • 如果 Mock 方法是由 IMocksControl 实例的 createMock 方法生成的,那么该 IMocksControl 实例方法 reset 的调用将会把所有该实例创建的 Mock 对象重新初始化。

在重新初始化之后,Mock 对象的状态将被置为 Record 状态。

7. EasyMock 预定义的参数匹配器

在使用 Mock 对象进行实际的测试过程中,EasyMock 会根据方法名和参数来匹配一个预期方法的调用。EasyMock 对参数的匹配默认使用 equals() 方法进行比较。这可能会引起一些问题。例如在上一章节中创建的mockStatement对象:

mockStatement.executeQuery("SELECT * FROM sales_order_table");
expectLastCall().andStubReturn(mockResultSet);

在实际的调用中,我们可能会遇到 SQL 语句中某些关键字大小写的问题,例如将 SELECT 写成 Select,这时在实际的测试中,EasyMock 所采用的默认匹配器将认为这两个参数不匹配,从而造成 Mock 对象的预期方法不被调用。EasyMock 提供了灵活的参数匹配方式来解决这个问题。如果您对 mockStatement 具体执行的语句并不关注,并希望所有输入的字符串都能匹配这一方法调用,您可以用 org.easymock.EasyMock 类所提供的 anyObject 方法来代替参数中的 SQL 语句:

mockStatement.executeQuery( anyObject() );
expectLastCall().andStubReturn(mockResultSet);

anyObject 方法表示任意输入值都与预期值相匹配。除了 anyObject 以外,EasyMock还提供了多个预先定义的参数匹配器,其中比较常用的一些有:

  • aryEq(X value):通过Arrays.equals()进行匹配,适用于数组对象;
  • isNull():当输入值为Null时匹配;
  • notNull():当输入值不为Null时匹配;
  • same(X value):当输入值和预期值是同一个对象时匹配;
  • lt(X value), leq(X value), geq(X value), gt(X value):当输入值小于、小等于、大等于、大于预期值时匹配,适用于数值类型;
  • startsWith(String prefix), contains(String substring), endsWith(String suffix):当输入值以预期值开头、包含预期值、以预期值结尾时匹配,适用于String类型;
  • matches(String regex):当输入值与正则表达式匹配时匹配,适用于String类型。

3.4-总结

如果需要在单元测试中构建 Mock 对象来模拟协同模块或一些复杂对象,EasyMock 是一个可以选用的优秀框架。EasyMock 提供了简便的方法创建 Mock 对象:通过定义 Mock 对象的预期行为和输出,你可以设定该 Mock 对象在实际测试中被调用方法的返回值、异常抛出和被调用次数。通过创建一个可以替代现有对象的 Mock 对象,EasyMock 使得开发人员在测试时无需编写自定义的 Mock 对象,从而避免了额外的编码工作和因此引入错误的机会。

4-JMockit

4.1-JMockit简介

JMockit 是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。所以他能解决当测试的代码包含了一些静态方法,未实现方法,未实现接口的问题。

4.2-JMockit的使用

4.2.1-在Maven pom.xml配置

<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.38</version>
<scope>test</scope>
</dependency>

JUnit4.x及以下用户特别注意事项

如果你是通过mvn test来运行你的测试程序 , 请确保JMockit的依赖定义出现在JUnit的依赖之前。

<!-- 先声明jmockit的依赖 -->
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.36</version>
<scope>test</scope>
</dependency>
<!-- 再声明junit的依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>

4.2.2-JMockit的使用

1. Mock类

再讲述如何Mock类之前,我们先给出一个普通的类,这个类有static,final,native,private方法。以及一个非static/final/native/private的普通public 方法。

//一个普通类 
public class AnOrdinaryClass {

// 静态方法
public static int staticMethod() {
return 1;
}

// 普通方法
public int ordinaryMethod() {
return 2;
}

// final方法
public final int finalMethod() {
return 3;
}

// native方法,返回4
public native int navtiveMethod();

// private方法
private int privateMethod() {
return 5;
}

// 调用private方法
public int callPrivateMethod() {
return privateMethod();
}
}

下面,讲述2种Mock类的方法

  • 用Expectations来Mock。
 //用Expectations来mock类
public class ClassMockingByExpectationsTest {

@Test
public void testClassMockingByExpectation() {
AnOrdinaryClass instanceToRecord = new AnOrdinaryClass();
new Expectations(AnOrdinaryClass.class) {
{
// mock静态方法
AnOrdinaryClass.staticMethod();
result = 10;
// mock普通方法
instanceToRecord.ordinaryMethod();
result = 20;
// mock final方法
instanceToRecord.finalMethod();
result = 30;
// native, private方法无法用Expectations来Mock
}
};
AnOrdinaryClass instance = new AnOrdinaryClass();
Assert.assertTrue(AnOrdinaryClass.staticMethod() == 10);
Assert.assertTrue(instance.ordinaryMethod() == 20);
Assert.assertTrue(instance.finalMethod() == 30);
// 用Expectations无法mock native方法
Assert.assertTrue(instance.navtiveMethod() == 4);
// 用Expectations无法mock private方法
Assert.assertTrue(instance.callPrivateMethod() == 5);
}

@BeforeClass
// 加载AnOrdinaryClass类的native方法的native实现
public static void loadNative() throws Throwable {
JNITools.loadNative();
}
}
  • 用MockUp来Mock类
//用MockUp来mock类
public class ClassMockingByMockUpTest {
// AnOrdinaryClass的MockUp类,继承MockUp即可
public static class AnOrdinaryClassMockUp extends MockUp<AnOrdinaryClass> {
// Mock静态方法
@Mock
public static int staticMethod() {
return 10;
}

// Mock普通方法
@Mock
public int ordinaryMethod() {
return 20;
}

@Mock
// Mock final方法
public final int finalMethod() {
return 30;
}

// Mock native方法
@Mock
public int navtiveMethod() {
return 40;
}

// Mock private方法
@Mock
private int privateMethod() {
return 50;
}
}

@Test
public void testClassMockingByMockUp() {
new AnOrdinaryClassMockUp();
AnOrdinaryClass instance = new AnOrdinaryClass();
// 静态方法被mock了
Assert.assertTrue(AnOrdinaryClass.staticMethod() == 10);
// 普通方法被mock了
Assert.assertTrue(instance.ordinaryMethod() == 20);
// final方法被mock了
Assert.assertTrue(instance.finalMethod() == 30);
// native方法被mock了
Assert.assertTrue(instance.navtiveMethod() == 40);
// private方法被mock了
Assert.assertTrue(instance.callPrivateMethod() == 50);
}

@BeforeClass
// 加载AnOrdinaryClass类的native方法的native实现
public static void loadNative() throws Throwable {
JNITools.loadNative();
}
}
2. Mock实例

在Mock类的章节中,我们知道了如何用Expectations来Mock类。Mock实例的用法基本一样。只需要把Expectations的构造函数参数换成实例即可。

用Expectations来Mock类与用Expectations来Mock实例的唯一不同就在于,前者影响类的所有实例,而后者只影响某一个实例。

//mock实例
public class InstanceMockingByExpectationsTest {
@Test
public void testInstanceMockingByExpectation() {
AnOrdinaryClass instance = new AnOrdinaryClass();
// 直接把实例传给Expectations的构造函数即可Mock这个实例
new Expectations(instance) {
{
// 尽管这里也可以Mock静态方法,但不推荐在这里写。静态方法的Mock应该是针对类的
// mock普通方法
instance.ordinaryMethod();
result = 20;
// mock final方法
instance.finalMethod();
result = 30;
// native, private方法无法用Expectations来Mock
}
};
Assert.assertTrue(AnOrdinaryClass.staticMethod() == 1);
Assert.assertTrue(instance.ordinaryMethod() == 20);
Assert.assertTrue(instance.finalMethod() == 30);
// 用Expectations无法mock native方法
Assert.assertTrue(instance.navtiveMethod() == 4);
// 用Expectations无法mock private方法
Assert.assertTrue(instance.callPrivateMethod() == 5);
}

@BeforeClass
// 加载AnOrdinaryClass类的native方法的native实现
public static void loadNative() throws Throwable {
JNITools.loadNative();
}
}
3. Mock接口

在讲述如何Mock接口前,我们给出一个普通接口的代码。

//一个普通的接口
public interface AnOrdinaryInterface {
// 方法1
public int method1();

// 方法2
public int method2();
}

我们依然给出2种Mock接口的2种方法。

  1. 用Expectations来Mock
  2. ```java
    //用Expectations来mock接口
    public class InterfaceMockingByExpectationsTest {
    // 通过@Injectable,让JMockit帮我们生成这个接口的实例,
    // 一般来说,接口是给类来依赖的,我们给待测试的类加上@Tested,就可以让JMockit做依赖注入。详细见JMockit基础的章节
    @Injectable
    AnOrdinaryInterface anOrdinaryInterface;
    
    @Test
    public void testInterfaceMockingByExpectation() {
        // 录制
        new Expectations() {
            {
                anOrdinaryInterface.method1();
                result = 10;
                anOrdinaryInterface.method2();
                result = 20;
            }
        };
        Assert.assertTrue(anOrdinaryInterface.method1() == 10);
        Assert.assertTrue(anOrdinaryInterface.method2() == 20);
    }
    
    }

    3. 用MockUp来Mock
    4. ```java
    //用MockUp来mock接口
    public class InterfaceMockingByMockUpTest {

    @Test
    public void testInterfaceMockingByMockUp() {
    // 手工通过MockUp创建这个接口的实例
    AnOrdinaryInterface anOrdinaryInterface = new MockUp<AnOrdinaryInterface>(AnOrdinaryInterface.class) {
    // 对方法Mock
    @Mock
    public int method1() {
    return 10;
    }

    @Mock
    public int method2() {
    return 20;
    }
    }.getMockInstance();

    Assert.assertTrue(anOrdinaryInterface.method1() == 10);
    Assert.assertTrue(anOrdinaryInterface.method2() == 20);
    }
    }

显然, 在Mock接口时,使用@Injectable注解API,比使用MockUp更方便。单纯通过MockUp生成接口的某个Mock实例,在实际的测试场景中并没有多大用途,接口就是用来给类依赖的,我们要充分利用JMockit的依赖注入功能。

4. @Mocked模拟方式和

@Mocked模拟,由录制、回放、验证三步骤完成,是对某个类的所有实例的所有方法进行完整的模拟方式。

@Mocked不仅能修饰一个类,也能修饰接口。@Mocked修饰的类/接口,是告诉JMockit,帮我生成一个Mocked对象,这个对象方法(包含静态方法)返回默认值。

  • Demo类

    public class HelloJMockit {

    public String sayHello() {
    Locale locale = Locale.getDefault();
    if (locale.equals(Locale.CHINA)) {
    // 在中国,就说中文
    return "你好世界";
    } else {
    // 在其它国家,就说英文
    return "Hello World";
    }
    }
    }
  • JMockit测试类

    public class HelloJMockitTest {
    @Mocked
    HelloJMockit helloJMockit;

    @Test
    public void sayHello1() {
    // 录制(Record)
    new Expectations() {
    {
    helloJMockit.sayHello();
    // 期待上述调用的返回是"hello,david",而不是返回实际返回值
    result = "hello david";
    }
    };
    // 重放(Replay)
    String msg = helloJMockit.sayHello();
    Assert.assertTrue(msg.equals("hello david"));
    // 验证(Verification)
    new Verifications() {
    {
    helloJMockit.sayHello();
    // 验证helloJMockit.sayHello()这个方法调用了1次
    times = 1;
    }
    };
    }
    }

    上面的方法也可以按需写成如下格式:

    public void sayHello1(@Mocked HelloJMockit helloJMockit) {…}
5. @Injectable模拟方式和@Tested

@Injectable 也是告诉 JMockit生成一个Mocked对象,但@Injectable只是针对其修饰的实例,而@Mocked是针对其修饰类的所有实例。此外,@Injectable对类的静态方法,构造函数没有影响。因为它只影响某一个实例。

@Injectable和@Mocked的方式很像,区别是@Injectable仅仅对当前实例进行模拟。

Mocked与Injectable区别:

  • Mocked 注入的依赖,类的所有实例都被mock,record的方法,在replay时,按照record的结果返回;没有record的方法返回默认值。
  • Injectable 注入的依赖,只mock指定的实例,record的方法,在replay时,按照record的结果返回;没有record的方法返回默认值。没有mock的实例,调用其原始方法。

@Tested和@Injectable通常搭配使用。若@Tested的构造函数有参数,则JMockit通过在测试属性、测试参数中查找@Injectable修饰的Mocked对象注入@Tested对象的构造函数来实例化,不然,则用无参构造函数来实例化。

除了构造函数的注入,JMockit还会通过属性查找的方式,把@Injectable对象注入到@Tested对象中。注入的匹配规则:先类型,再名称(构造函数参数名,类的属性名)。若找到多个可以注入的@Injectable,则选择最优先定义的@Injectable对象。当然,我们的测试程序要尽量避免这种情况出现。因为给哪个测试属性/测试参数加@Injectable,是人为控制的。

  • 订单类

    public class OrderService {
    // 短信服务类,用于向某用户发短信。
    @Autowired MessageService messageService;
    // 用户服务类,用于校验某个用户是不是合法用户
    @Autowired UserService userService;
    // 下单
    public boolean submitOrder(long userId) {
    // 先校验用户身份
    if (!userService.check(userId)) {
    // 用户身份不合法
    return false;
    }
    // 下单
    this.saveOrder(order);// TODO 逻辑略…
    // 下单完成,给买家发短信
    if (this.messageService.sendMessage(userId, "下单成功")) {
    // 短信发送成功
    return true;
    }
    return false;
    }
    }
  • 测试类

    public class TestedAndInjectable {
    //@Tested修饰的类,表示是我们要测试对象。JMockit会帮我们实例化这个测试对象
    @Tested
    OrderService orderService;

    // 测试注入方式
    @Test
    public void testSubmitOrder(@Injectable MessageService messageService,
    @Injectable UserCheckService userCheckService,
    @Injectable Order testOrder) {
    long testUserId = 123l;
    //实例化MessageService,userCheckService,通过OrderService属性,注入对象中;
    new Expectations() {
    {
    // 当向testUserId发短信时,假设都发成功了
    messageService.sendMessage(testUserId, anyString);
    result = true;
    // 当检验testUserId的身份时,假设该用户都是合法的
    userCheckService.check(testUserId);
    result = true;
    }
    };
    Order testOrder = new Order("嘟嘟机器人", 996)
    Assert.assertTrue(orderService.submitOrder(testUserId, testOrder));
    }
    }
6. @Capturing

@Capturing主要用于子类/实现类的Mock, 我们只知道父类或接口时,但我们需要控制它所有子类的行为时,子类可能有多个实现(可能有人工写的,也可能是AOP代理自动生成时),就用@Capturing

7. MockUp和@Mock

这种方式非常简单,直接,很多程序员们都喜欢用,掌握了MockUp和@Mock能帮我们解决大部分的Mock场景。

案例如下:

class MockUpTest {
@Test
public void testMockUp() {
// 对Java自带类Calendar的get方法进行定制
// 只需要把Calendar类传入MockUp类的构造函数即可
new MockUp<Calendar>(Calendar.class) {
// 想Mock哪个方法,就给哪个方法加上@Mock, 没有@Mock的方法,不受影响
@Mock
public int get(int unit) {
if (unit == Calendar.YEAR) {
return 2017;
}
if (unit == Calendar.MONDAY) {
return 1;
}
return 0;
}
};
// 从此Calendar的get方法,就沿用你定制过的逻辑,而不是它原先的逻辑。
Calendar cal = Calendar.getInstance(Locale.FRANCE);
Assert.assertTrue(cal.get(Calendar.YEAR) == 2017);
Assert.assertTrue(cal.get(Calendar.MONDAY) == 1);
// Calendar的其它方法,不受影响
Assert.assertTrue((cal.getFirstDayOfWeek() == Calendar.MONDAY));
}
}

MockUp和@Mock比较适合于一个项目中,用于对一些通用类的Mock,以减少大量重复的new Exceptations代码。

在实际Mock场景中,我们需要灵活运用JMockit其它的Mock API。让我们的Mock程序简单,高效。

一个类有多个实例,但只对其中某1个实例进行mock的场景是MockUp和@Mock做不到的,这种时候就需要上述的@Capturing注解了。模拟接口,MockUp不支持,采用@Capturing。

8. Expectations

Expectations的作用主要是用于录制。即录制类/对象的调用,返回值是什么。主要有两种使用方式:

  • 通过引用外部类的Mock对象(@Injectabe,@Mocked,@Capturing)来录制;

  • 通过构建函数注入类/对象来录制。

9. Verifications

Verifications是用于做验证。验证Mock对象(即@Moked/@Injectable@Capturing修饰的或传入Expectation构造函数的对象)有没有调用过某方法,调用了多少次。

通常在实际测试程序中,我们更倾向于通过JUnit/TestNG/SpringTest的Assert类对测试结果的验证, 对类的某个方法有没调用,调用多少次的测试场景并不是太多。因此在验证阶段,我们完全可以用JUnit/TestNG/SpringTest的Assert类取代new Verifications()验证代码块。除非,你的测试程序关心类的某个方法有没有调用,调用多少次,你可以使用new Verifications()验证代码块。

常见用法

案例类(这个类有public,static,final,private方法):

class AnOrdinaryClass {
// 普通方法
public int ordinaryMethod() {
return 1;
}

// 静态方法
public static int staticMethod() {
return 2;
}

// final方法
public final int finalMethod() {
return 3;
}

// private方法
private int privateMethod() {
return 4;
}

// 调用private方法
public int callPrivateMethod() {
return this.privateMethod();
}
}
  • 测试类1(用Expectations来Mock):

    class ClassMockingByExpectationsTest {
    @Test
    public void testClassMockingByExpectation() {
    AnOrdinaryClass instanceToRecord = new AnOrdinaryClass();
    new Expectations(AnOrdinaryClass.class) {
    {
    // mock普通方法
    instanceToRecord.ordinaryMethod();
    result = 11;
    // mock静态方法
    AnOrdinaryClass.staticMethod();
    result = 22;
    // mock final方法
    instanceToRecord.finalMethod();
    result = 33;
    // private方法无法用Expectations来Mock
    }
    };
    AnOrdinaryClass instance = new AnOrdinaryClass();
    Assert.assertTrue(instance.ordinaryMethod() == 11);
    Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
    Assert.assertTrue(instance.finalMethod() == 22);
    // 用Expectations无法mock private方法
    Assert.assertTrue(instance.callPrivateMethod() == 4);
    }
    }
  • 测试类2(用MockUp来Mock):

    class ClassMockingByMockUpTest {
    // AnOrdinaryClass的MockUp类,继承MockUp即可
    public static class AnOrdinaryClassMockUp extends MockUp<AnOrdinaryClass> {
    // Mock普通方法
    @Mock
    public int ordinaryMethod() {
    return 11;
    }

    // Mock静态方法
    @Mock
    public static int staticMethod() {
    return 22;
    }

    @Mock
    // Mock final方法
    public final int finalMethod() {
    return 33;
    }

    // Mock private方法
    @Mock
    private int privateMethod() {
    return 44;
    }
    }

    @Test
    public void testClassMockingByMockUp() {
    new AnOrdinaryClassMockUp();
    AnOrdinaryClass instance = new AnOrdinaryClass();
    // 普通方法被mock了
    Assert.assertTrue(instance.ordinaryMethod() == 11);
    // 静态方法被mock了
    Assert.assertTrue(AnOrdinaryClass.staticMethod() == 22);
    // final方法被mock了
    Assert.assertTrue(instance.finalMethod() == 33);
    // private方法被mock了
    Assert.assertTrue(instance.callPrivateMethod() == 44);
    }
    }

4.3-总结

建议使用MockUp & @Mock方法来写单元测试,JUnit的Assert类对测试结果的验证,并且灵活运用JMockit其它的Mock API。

5-PowerMock

5.1-PowerMock简介

PowerMock是一个框架,它以更强大的功能扩展了其他模拟库,例如EasyMock。PowerMock使用自定义的类加载器和字节码操作来模拟静态方法,构造函数,最终类和方法,私有方法,删除静态初始化程序等。通过使用自定义类加载器,无需对IDE或持续集成服务器进行任何更改,从而简化了采用过程。熟悉受支持的模拟框架的开发人员会发现PowerMock易于使用,因为整个期望API都是相同的,无论是静态方法还是构造函数。PowerMock旨在通过少量方法和注释扩展现有的API,以启用额外的功能。当前,PowerMock支持EasyMock和Mockito。

在编写单元测试时,绕过封装通常很有用,因此PowerMock包括一些简化了反射的功能,这些反射对于测试特别有用。这样可以轻松访问内部状态,还可以简化部分和私有模拟。

PowerMock支持JUnit和TestNG,扩展了EasyMock和Mockito框架,增加了mock static、final方法的功能。

5.2-Mock底层原理

  • Mockito原理

    Mockito底层使用了动态代理,用到了CGLIB。因此需要被mock的对象,Mockito都会生成一个子类继承该类,这也就是为什么final类、private方法、static方法不可以被Mock的原因

  • PowerMock原理

    PowerMock有两个重要的依赖:javassist和objenesis。

    javassist是一个修改java字节码的工具包,objenesis是一个绕过构造方法来实例化一个对象的工具包。由此看来,PowerMock的本质是通过修改字节码来实现对静态和final等方法的mock的

    下面是PowerMock的简单实现原理:

    • 当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。
    • PowerMock会根据你的mock要求,去修改写在注解@PrepareForTest里的class文件(当前测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。
    • 如果需要mock的是系统类的final方法和静态方法,PowerMock不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。

5.3-PowerMock的使用

5.3.1-添加Maven依赖

Junit4.4版本及以上

<properties>
<powermock.version>2.0.2</powermock.version>
</properties>
<dependencies>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

5.3.2-PowerMock的API用法

1. PowerMock注解@RunWith@PrepareForTest
@RunWith(PowerMockRunner.class)
@PrepareForTest(RequestUtils.class)

@RunWith(PowerMockRunner.class)语句告诉JUnit用PowerMockRunner来执行测试。
@PrepareForTest(RequestUtils.class)语句告诉PowerMock准备RequestUtils类进行测试。适用于模拟final类或有final, private, static, native方法的类
@PrepareForTest是当使用PowerMock强大的Mock静态、final、private方法时,需要添加的注解。
如果测试用例里没有使用注解@PrepareForTest,可以不加注解@RunWith(PowerMockRunner.class),反之亦然。

2. 模拟静态方法
  • 如何mock和stub

    @PrepareForTest(Static.class) // Static.class包含静态方法
  • 调用PowerMockito.mockStatic()以模拟静态类(用于PowerMockito.spy(class)模拟特定方法)

    PowerMockito.mockStatic(Static.class);
  • 只需使用Mockito.when()来设置您的期望:

    Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);

注意:如果需要模拟java系统/ bootstrap类加载器(在java.lang或java.net等中定义的类)加载的类,则需要使用方法。

2.1 如何验证行为

静态方法的验证分两个步骤进行。

  1. 首先调用PowerMockito.verifyStatic(Static.class)以开始验证行为,然后
  2. 调用的静态方法Static.class进行验证。例如:
PowerMockito.verifyStatic(Static.class); // 1
Static.firstStaticMethod(param); // 2

重要提示:您需要verifyStatic(Static.class)按方法调用。

2.2 如何使用参数匹配器

Mockito匹配器可能仍适用于PowerMock模拟。例如,对每个模拟的静态方法使用自定义参数匹配器:

PowerMockito.verifyStatic(Static.class);
Static.thirdStaticMethod(Mockito.anyInt());
2.3 如何验证确切的电话号码

您仍然可以将Mockito.VerificationMode(例如Mockito.times(x))与结合使用PowerMockito.verifyStatic(Static.class, Mockito.times(2))

PowerMockito.verifyStatic(Static.class, Mockito.times(1));
2.4 如何将void静态方法存根以引发异常

如果不是私人的,请执行以下操作:

PowerMockito.doThrow(new ArrayStoreException("Mock error")).when(StaticService.class);
StaticService.executeMethod();

请注意,您可以对最终课程/方法执行相同的操作:

PowerMockito.doThrow(new ArrayStoreException("Mock error")).when(myFinalMock).myFinalMethod();

对于私有方法,请使用PowerMockito.when,例如:

when(tested, "methodToExpect", argument).thenReturn(myReturnValue);
2.5 模拟,存根和验证静态方法的完整示例
@RunWith(PowerMockRunner.class)
@PrepareForTest(Static.class)
public class YourTestCase {
@Test
public void testMethodThatCallsStaticMethod() {
//模拟Static类中的所有静态方法
PowerMockito.mockStatic(Static.class);
//使用Mockito设置期望
Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);
Mockito.when(Static.secondStaticMethod()).thenReturn(123);

//执行测试类
classCallStaticMethodObj.execute();

//与Mockito不同,始终首先使用PowerMockito.verifyStatic(Class)
//开始验证行为
PowerMockito.verifyStatic(Static.class, Mockito.times(2));
//重要提示:调用要验证Static 的static方法
Static.firstStaticMethod(param);


//重要信息:您需要在每个方法验证中调用verifyStatic(Class)
//因此再次调用verifyStatic(Class)
PowerMockito.verifyStatic(Static.class); // default times is once
//再次调用被验证为
Static.secondStaticMethod();

//同样,请记住调用verifyStatic(Class)
PowerMockito.verifyStatic(Static.class, Mockito.never());
//然后再次调用static方法
Static.thirdStaticMethod();
}
}
3. 部分模拟

您可以使用PowerMockito使用PowerMockito.spy来部分模拟方法。请小心(以下内容摘自Mockito文档,同样适用于PowerMockito):

有时,无法使用标准when(..)方法对间谍进行打桩。例:

List list = new L/您必须使用doReturn()对进行stubinkedList();
List spy = spy(list);
//不可能的:真正的方法调用,所以spy.get(0)抛出IndexOutOfBoundsException异常(名单又是空的)
when(spy.get(0)).thenReturn("foo");

/您必须使用doReturn()对进行stub
doReturn("foo").when(spy).get(0);
4. 如何验证行为

只需使用Mockito.vertify()进行标准验证:

Mockito.verify(mockObj, times(2)).methodToMock();
5. 如何验证私人行为

使用PowerMockito.verifyPrivate(),例如

verifyPrivate(tested).invoke("privateMethodName", argument1);

这也适用于私有静态方法。

6. 如何模拟新对象的构造

使用PowerMockito.whenNew,例如

whenNew(MyClass.class).withNoArguments().thenThrow(new IOException("error message"));

请注意,您必须准备创建MyClass用于测试的新实例的类,而不是其MyClass本身。例如,如果正在执行的类new MyClass()称为X,则必须先进行操作@PrepareForTest(X.class)才能whenNew工作:

@RunWith(PowerMockRunner.class)
@PrepareForTest(X.class)
public class XTest {
@Test
public void test() {
whenNew(MyClass.class).withNoArguments().thenThrow(new IOException("error message"));

X x = new X();
x.y(); // y is the method doing "new MyClass()"

..
}
}
7. 如何验证新对象的构造

使用PowerMockito.verifyNew,例如

verifyNew(MyClass.class).withNoArguments();
8. 如何使用参数匹配器

Mockito匹配器可能仍适用于PowerMock模拟:

Mockito.verify(mockObj).methodToMock(Mockito.anyInt());  
9. 完整的spying实例
@RunWith(PowerMockRunner.class)
//我们制备用于测试PartialMockClass因为它最终还是我们需要模拟私有或静态方法
@PrepareForTest(PartialMockClass.class)
public class YourTestCase {
@Test
public void spyingWithPowerMock() {
PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());

//使用Mockito设置期望
Mockito.when(classUnderTest.methodToMock()).thenReturn(value);
//执行测试
classUnderTest.execute();
//使用Mockito.verify()验证结果
Mockito.verify(mockObj, times(2)).methodToMock();
}
}
10. 私有方法的部分模拟的完整示例
@RunWith(PowerMockRunner.class)
//我们制备用于测试PartialMockClass因为它最终还是我们需要模拟私有或静态方法
@PrepareForTest(PartialMockClass.class)
public class YourTestCase {
@Test
public void privatePartialMockingWithPowerMock() {
PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());

//使用PowerMockito设置期望
PowerMockito.doReturn(value).when(classUnderTest, "methodToMock", "parameter1");
//执行测试
classUnderTest.execute();
//使用PowerMockito.verify()验证结果
PowerMockito.verifyPrivate(classUnderTest, times(2)).invoke("methodToMock", "parameter1");
}
}

5.3.3-PowerMock的使用

1. 测试static方法
  • 被测试类

    public class Employee {
    public static int count() {
    throw new UnsupportedOperationException();
    }
    }

    public class EmployeeService {
    public int getEmployeeCount() {
    return Employee.count();
    }
    }
  • 测试类

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Employee.class)
    public class EmployeeServiceTest {

    @Test
    public void shouldReturnTheCountOfEmployeesUsingTheDomainClass() {

    PowerMockito.mockStatic(Employee.class);
    PowerMockito.when(Employee.count()).thenReturn(900);

    EmployeeService employeeService = new EmployeeService();
    assertEquals(900, employeeService.getEmployeeCount());

    }
    }

    注意这里使用的是mockStatic而不是的mock。

2. 测试返回void的静态方法
  • 被测试类

    public class Employee {
    public static void giveIncrementOf(int percentage) {
    throw new UnsupportedOperationException();
    }
    }
    public class EmployeeService{
    public boolean giveIncrementToAllEmployeesOf(int percentage) {
    try{
    Employee.giveIncrementOf(percentage);
    return true;
    } catch(Exception e) {
    return false;
    }
    }
    }
  • 测试类

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Employee.class)
    public class EmployeeServiceTest {

    @Test
    public void shouldReturnTrueWhenIncrementOf10PercentageIsGivenSuccessfully() {
    PowerMockito.mockStatic(Employee.class);
    PowerMockito.doNothing().when(Employee.class);
    Employee.giveIncrementOf(10);
    EmployeeService employeeService = new EmployeeService();
    assertTrue(employeeService.giveIncrementToAllEmployeesOf(10));
    }

    @Test
    public void shouldReturnFalseWhenIncrementOf10PercentageIsNotGivenSuccessfully() {
    PowerMockito.mockStatic(Employee.class);
    PowerMockito.doThrow(new IllegalStateException()).when(Employee.class);
    Employee.giveIncrementOf(10);
    EmployeeService employeeService = new EmployeeService();
    assertFalse(employeeService.giveIncrementToAllEmployeesOf(10));
    }
    }
3. PowerMockito.doNothing 与PowerMockito.doThrow

PowerMockito.doNothing方法告诉PowerMock下一个方法调用时什么也不做。

PowerMockito.doThrow方法告诉PowerMock下一个方法调用时产生异常。

PowerMock使用自定义类加载器和字节码操作来模拟静态方法。对于实例中没有mock的方法,也有默认返回值,比如返回int类型的方法,默认返回0
PowerMockito.doNothing和PowerMockito.doThrow的语法可用于实例方法。

  • 被测试类

    public class Employee {
    public void save() {
    throw new UnsupportedOperationException();
    }
    }
  • 测试类

    public class EmployeeTest {
    @Test()
    public void shouldNotDoAnythingIfEmployeeWasSaved() {
    Employee employee = PowerMockito.mock(Employee.class);
    PowerMockito.doNothing().when(employee).save();
    try {
    employee.save();
    } catch(Exception e) {
    fail("Should not have thrown an exception");
    }
    }

    @Test(expected = IllegalStateException.class)
    public void shouldThrowAnExceptionIfEmployeeWasNotSaved() {
    Employee employee = PowerMockito.mock(Employee.class);
    PowerMockito.doThrow(new IllegalStateException()).when(employee).save();
    employee.save();
    }
    }

注意这里doThrow和doNothing方法不会对下一行产生影响。

4. 验证方法调用
  • 被测试类

    public class Employee{
    public boolean isNew() {
    throw new UnsupportedOperationException();
    }

    public void update() {
    throw new UnsupportedOperationException();
    }

    public void create() {
    throw new UnsupportedOperationException();
    }

    public static void giveIncrementOf(int percentage) {
    throw new UnsupportedOperationException();
    }
    }
    public class EmployeeService{
    public void saveEmployee(Employee employee) {
    if(employee.isNew()) {
    employee.create();
    return;
    }
    employee.update();
    }
    }
  • 测试类

    public class EmployeeServiceTest {
    @Test
    public void shouldCreateNewEmployeeIfEmployeeIsNew() {

    Employee mock = PowerMockito.mock(Employee.class);
    PowerMockito.when(mock.isNew()).thenReturn(true);
    EmployeeService employeeService = new EmployeeService();
    employeeService.saveEmployee(mock);
    Mockito.verify(mock).create();
    Mockito.verify(mock, Mockito.never()).update();
    }
    }

Mockito.verify(mock).create()验证调用了create方法。
Mockito.verify(mock, Mockito.never()).update();验证没有调用update方法。

  • 验证静态方法

    public class EmployeeServiceTest {
    @Test
    public void shouldInvoke_giveIncrementOfMethodOnEmployeeWhileGivingIncrement() {
    PowerMockito.mockStatic(Employee.class);
    PowerMockito.doNothing().when(Employee.class);
    Employee.giveIncrementOf(9);
    EmployeeService employeeService = new EmployeeService();
    employeeService.giveIncrementToAllEmployeesOf(9);
    PowerMockito.verifyStatic();
    Employee.giveIncrementOf(9);
    }
    }
5. 验证调用次数的方法:

Mockito.times(int n) : 准确的验证方法调用的次数:n
Mockito.atLeastOnce() : 验证方法至少调用1次
Mockito.atLeast(int n) : 验证方法最少调用n次
Mockito.atMost(int n): 验证方法最多调用n次
Mockito.inOrder:验证方法调用的顺序

@Test
public void shouldInvokeIsNewBeforeInvokingCreate() {
Employee mock = PowerMockito.mock(Employee.class);
PowerMockito.when(mock.isNew()).thenReturn(true);
EmployeeService employeeService = new EmployeeService();
employeeService.saveEmployee(mock);
InOrder inOrder = Mockito.inOrder(mock);
inOrder.verify(mock).isNew();
Mockito.verify(mock).create();
Mockito.verify(mock, Mockito.never()).update();
}
  • 测试final类或方法

    public final class EmployeeIdGenerator {

    public final static int getNextId() {
    throw new UnsupportedOperationException();
    }
    }
    public class Employee {
    public void setEmployeeId(int nextId) {
    throw new UnsupportedOperationException();
    }
    }
    public class EmployeeService {
    public void saveEmployee(Employee employee) {
    if(employee.isNew()) {
    employee.setEmployeeId(EmployeeIdGenerator.getNextId());
    employee.create();
    return;
    }
    employee.update();
    }
    }
  • 测试类

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(EmployeeIdGenerator.class)
    public class EmployeeServiceTest {

    @Test
    public void shouldGenerateEmployeeIdIfEmployeeIsNew() {

    Employee mock = PowerMockito.mock(Employee.class);
    PowerMockito.when(mock.isNew()).thenReturn(true);
    PowerMockito.mockStatic(EmployeeIdGenerator.class);
    PowerMockito.when(EmployeeIdGenerator.getNextId()).thenReturn(90);
    EmployeeService employeeService = new
    EmployeeService();
    employeeService.saveEmployee(mock);
    PowerMockito.verifyStatic();
    EmployeeIdGenerator.getNextId();
    Mockito.verify(mock).setEmployeeId(90);
    Mockito.verify(mock).create();
    }
    }
6. 测试构造方法
  • 被测试类

    public class WelcomeEmail {
    public WelcomeEmail(final Employee employee, final String message) {
    throw new UnsupportedOperationException();
    }
    public void send() {
    throw new UnsupportedOperationException();
    }
    }
    public class EmployeeService{
    public void saveEmployee(Employee employee) {
    if(employee.isNew()) {
    employee.setEmployeeId(EmployeeIdGenerator.getNextId());
    employee.create();
    WelcomeEmail emailSender = new WelcomeEmail(employee,
    "Welcome to Mocking with PowerMock How-to!");
    emailSender.send();
    return;
    }
    employee.update();
    }
    }
  • 测试类

    public class  WelcomeEmailTest {
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({EmployeeIdGenerator.class, EmployeeService.class})
    public class EmployeeServiceTest {

    @Test
    public void shouldSendWelcomeEmailToNewEmployees()throws Exception {
    Employee employeeMock =PowerMockito.mock(Employee.class);
    PowerMockito.when(employeeMock.isNew()).thenReturn(true);
    PowerMockito.mockStatic(EmployeeIdGenerator.class);
    WelcomeEmail welcomeEmailMock = PowerMockito.mock(WelcomeEmail.class);
    PowerMockito.whenNew(WelcomeEmail.class).withArguments(employeeMock, "Welcome").thenReturn(welcomeEmailMock);
    EmployeeService employeeService = new EmployeeService();
    employeeService.saveEmployee(employeeMock);

    PowerMockito.verifyNew(WelcomeEmail.class).withArguments(employeeMock, "Welcome");
    Mockito.verify(welcomeEmailMock).send();
    }
    }
    }

注意PowerMockito.verifyNew的第2个参数支持前面提到的验证模式。>PowerMockito.whenNew().withArguments(...).thenReturn()是对构造方法的mock模式
PowerMockito.verifyNew().withArguments()是验证模式。

7. 参数匹配

PowerMock使用equals方法验证参数。matcher可更加灵活的处理参数。
Mockito.eq;
Mockito.matches;
Mockito.any(anyBoolean , anyByte , anyShort , anyChar , anyInt ,anyLong , anyFloat , anyDouble , anyList , anyCollection , anyMap , anySet~);
Mockito.isNull;
Mockito.isNotNull;
Mockito.endsWith;
Mockito.isA;

8. 回答

在某些边缘的情况下不可能通过简单地通过PowerMockito.when().thenReturn()模拟,这时可以使用Answer接口。

 	@Test
public void shouldReturnCountOfEmployeesFromTheServiceWithDefaultAnswer() {
EmployeeService mock = PowerMockito.mock(EmployeeService.class, new Answer() {
public Object answer(InvocationOnMock invocation) {
return 10;
}
});
}
}

Answer接口指定执行的action和返回值执。
Answer的参数是InvocationOnMock的实例,支持:

callRealMethod():调用真正的方法
getArguments():获取所有参数
getMethod():返回mock实例调用的方法
getMock():获取mock实例

9. spy进行部分模拟
  • 被测试类

    public class EmployeeService{
    public void saveEmployee(Employee employee) {
    if(employee.isNew()) {
    createEmployee(employee);
    return;
    }
    employee.update();
    }

    void createEmployee(Employee employee) {
    employee.setEmployeeId(EmployeeIdGenerator.getNextId());
    employee.create();
    WelcomeEmail emailSender = new WelcomeEmail(employee,
    "Welcome");
    emailSender.send();
    }
    }
  • 测试类

    public class EmployeeServiceTest {
    @Test
    public void shouldInvokeTheCreateEmployeeMethodWhileSavingANewEmployee() {
    final EmployeeService spy = PowerMockito.spy(new EmployeeService());
    final Employee employeeMock = PowerMockito.mock(Employee.class);
    PowerMockito.when(employeeMock.isNew()).thenReturn(true);
    PowerMockito.doNothing().when(spy).createEmployee(employeeMock);
    spy.saveEmployee(employeeMock);
    Mockito.verify(spy).createEmployee(employeeMock);
    }
    }

    spy只能使用PowerMockito.doNothing()/doReturn()/doThrow()。

10. 模拟私有方法
  • 被测试类

    public class EmployeeService{
    private void createEmployee(Employee employee) {
    employee.setEmployeeId(EmployeeIdGenerator.getNextId());
    employee.create();
    WelcomeEmail emailSender = new WelcomeEmail(employee,
    "Welcome");
    emailSender.send();
    }
    }
  • 测试类

    public class   EmployeeServiceTest{
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({EmployeeIdGenerator.class, EmployeeService.class})
    public class EmployeeServiceTest {

    @Test
    public void shouldInvokeTheCreateEmployeeMethodWhileSavingANewEmployee() throws Exception {

    final EmployeeService spy = PowerMockito.spy(new EmployeeService());
    final Employee employeeMock = PowerMockito.mock(Employee.class);
    PowerMockito.when(employeeMock.isNew()).thenReturn(true);
    PowerMockito.doNothing().when(spy, "createEmployee", employeeMock);
    spy.saveEmployee(employeeMock);
    PowerMockito.verifyPrivate(spy).invoke("createEmployee", employeeMock);
    }
    }
    }

PowerMockito.suppress(PowerMockito.constructor(BaseEntity.class)):表示禁用BaseEntity的构造函数。

PowerMockito.suppress(PowerMockito.constructor(BaseEntity.class, String.class, Integer.class)):后面表示带字符串和整数参数。

PowerMockito.suppress(PowerMockito.method(BaseEntity.class, "performAudit", String.class)):表示禁用BaseEntity的performAudit方法。

@SuppressStaticInitializationFor("BaseEntity"):表示禁用BaseEntity的静态初始化。注意引号部分通常需要全名,如”com.gitshah.powermock.BaseEntity”。

PowerMockito.suppress(PowerMockito.field(BaseEntity.class:identifier”))`:禁用域。

6-SpringBoot整合Junit+Mockito+PowerMock

6.1-添加依赖

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
<scope>test</scope>
</dependency>

6.2-UUID加密算法类

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;

import java.util.UUID;

public class Encrypt {
/**
* 密码加密
* @param password 待加密的密码
* @param md5 是否先用 md5 加密
* @return 加密后的密码
*/
public static String passwordGenerator(String password) {
password = DigestUtils.md5Hex(password);
String salt = DigestUtils.sha1Hex(UUID.randomUUID().toString()).substring(0, 4);
String saltString = DigestUtils.sha1Hex(password + salt) + salt;
String encryPassword = Base64.encodeBase64String(saltString.getBytes());
return encryPassword;
}
}

其中,UUID.randomUUID()、DigestUtils.md5Hex()、DigestUtils.sha1Hex()、Base64.encodeBase64String() 均为静态方法,而 uuid.toString() 则为 UUID 实例对象的方法。

对于 UUID 的 mock,需要两步:

第一步,是使用 mockito mock 一个 UUID 对象,并 mock 其在代码中使用的方法,这里要 mock 的是 toString() 方法。

第二步,是使用 powormockito,mock UUID 的 randomUUID() 方法,使其返回上一步 mock 的 uuid 对象,这样我们就能得到预期的 uuid 的方法(这里是 toString)的执行结果。

DigestUtils 和 Base64 的静态方法直接使用 powermockito 来 mock 就可以。

6.3-测试类

@RunWith(PowerMockRunner.class)
@PrepareForTest({UUID.class, DigestUtils.class, Encrypt.class, Base64.class})
public class EncryptTest {

@Test
public void passwordGeneratorWithMd5() {
String randomString = "abcdefg";
String salt = "abcd";
String password = "123456";
String rePassword = "654321";
String twiceSaltMd5 = "hellomd5";

// mock uuid 对象,使其 toString() 方法返回预定义的 randomString 字符串
UUID uuid = PowerMockito.mock(UUID.class);
Mockito.when(uuid.toString()).thenReturn(randomString);

// mock UUID 类,使其 randomUUID() 方法返回刚刚 mock 的 uuid 对象
PowerMockito.mockStatic(UUID.class);
PowerMockito.when(UUID.randomUUID()).thenReturn(uuid);

// mock DigestUtils 类,使其 sha1Hex() 方法在接收预定义的 randomString 参数时,返回预定义的 salt 字符串
PowerMockito.mockStatic(DigestUtils.class);
PowerMockito.when(DigestUtils.sha1Hex(randomString)).thenReturn(salt);

// 使 mock 的 DigestUtils 类的 md5Hex 方法,在接受预定义的 password 时,生成预定义的 rePassword 字符串
PowerMockito.when(DigestUtils.md5Hex(password)).thenReturn(rePassword);

// 使 mock 的 DigestUtils 类的 sha1Hex() 方法在接收预定义的 rePassword 和 salt 时,返回 预定义的 twiceSaltMd5 字符串
PowerMockito.when(DigestUtils.sha1Hex(rePassword + salt)).thenReturn(twiceSaltMd5);

// mock Base64 类,使其encodeBase64String() 方法在接收 预定义的串时,返回预定义的加密后密码
PowerMockito.mockStatic(Base64.class);
String imencryptpassword = "imencryptpasswordwithmd5";
PowerMockito.when(Base64.encodeBase64String((twiceSaltMd5 + salt).getBytes())).thenReturn(imencryptpassword);

// 调用加密方法,并验证结果
String encryptPassword = Encrypt.passwordGenerator("123456");
assertEquals(imencryptpassword, encryptPassword);
}
}

7-Mock测试工具对比

目前,这个级别的mock工具有easymock、jMock、Mockito、Unitils Mock、PowerMock、JMockit等等.
关于它们的优劣势在JMockit官网上给出一个简单的比较,结果如下图所示。这个结果可能会偏向JMockit,我们可以作为参考。

FeatureEasyMockjMockMockitoUnitils MockPowerMock: EasyMock APIPowerMock: Mockito APIJMock it
Invocation count constraints(调用数限制)
Recording strict expectations(记录严格的预期结果)
Explicit verification(显式验证)
Partial mocking(部分mock)
No method call to switch from record to replay(切换记录回放时无方法调用)
No extra code for implicit verification(隐式验证没有额外代码)N/AN/AN/A
No extra “prepare for test” code(没有额外的”prepare for test”代码)
No need to use @RunWith annotation or base test class(不需要用@runwith注解和测试基类)
Consistent syntax between void and non-void methods(空和非空方法的语法一致)
Argument matchers for some parameters only, not all
Easier argument matching based on properties of value objects(基于值对象属性的简化参数匹配)
Cascading mocks(级联mock)
Support for mocking multiple interfaces(多接口mock)
Support for mocking annotation types(注释类型mock)
Partially ordered expectations
Mocking of constructors and final/static/native/private methods(构造函数、final、static和private方法的mock)
Declarative application of mocks/stubs to whole test classes
Auto-injection of mocks(mock的自动注入)
Mocking of “new-ed” objects(“new-ed”对象的mock)
Support for mocking enum types
Declarative mocks for the test class (mock fields)
Declarative mocks for test methods (parameters, local fields)
Special fields for “any” argument matching
Use of an special field to specify invocation results
Use of special fields to specify invocation count constraints
Expectations with custom error messages
On-demand mocking of unspecified implementation classes
Capture of instances created by code under test
Recording & verification of expectations in loops
Support for covariant return types
“Duck typing” mocks for state-based tests
Single jar file in the classpath is sufficient to use mocking API(在classpath中的单个jar文件就能够使用mockAPI)N/AN/A
Total6/327/3213/3111/319/3114/3032/32
Total when ignoring JMockit-only features6/227/2213/2111/219/2114/2022/22

就目前来讲,是mockit+powermock、JMockit这两种工具使用人数较多。JMockit的功能最为完善,mockit+powermock的用户体验相对较好一点。