暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

史上最全,一篇文章搞定Mock原理及应用

Jerry分享汇 2021-04-12
11398

史上最全,一篇文章搞定Mock原理及应用

目录

一、Mock基础知识

    什么是Mock 

    Mock的目的

    Mock的应用场景

    Mock框架对比(JAVA)

二、Mockito研究

    主要资源

    环境起步

    核心注解

    常用方法

三、MockMVC研究

    使用目的

    环境起步

    运行配置

    运行机制

    创建Builder

    方法解析

四、Mock实例

    MockMVC实例

    Mockito实例

附录:参考文档一览




一、Mock基础知识

什么是Mock

Mock 是在测试过程中,对于一些不容易构造/获取的对象,创建一个Mock 对象来模拟对象的行为。Mock 最大的功能是帮你把单元测试进行解耦,如果你的代码对另一个类或者接口有依赖,它能够帮你模拟这些依赖,并帮你验证所调用的依赖的行为。

Mock的目的

  1. 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么,返回值是什么等等

  2. 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作等等

Mock的应用场景

  1. 团队并行开发,有了Mock,前、后端人员只需要定义好接口文档就可以开始并行工作,互不影响。后端与后端之间如果有接口耦合,也同样使用Mock解决。

  2. 开启TDD模式,即测试驱动开发。单元测试是TDD实现的基石,而TDD经常会碰到协同模块尚未开发完成的情况,但是有了mock,这些一切都不是问题。当接口定义好后,测试人员就可以创建一个Mock,把接口添加到自动化测试环境,提前创建测试。

  3. 隔离系统,假如需要调用一个post请求,为了获得响应,来看当前系统是否能正确处理返回的“响应”,但是这个post请求会造成数据库中数据的污染,那么就可以充分利用Mock,构造一个虚拟的post请求,指定返回的数据就OK。

  4. 模拟访问资源,比如说,你需要调用一个“墙”外的资源来方便自己调试,就可以自己Mock一个。

  5. 系统演示,假如我们需要创建一个演示系统,并且做了简单的UI,那么在完全没有开发后端服务的情况下,也可以进行演示。

  6. 测试场景

    • 接口测试**:对一个接口进行测试,排除这个接口对外部环境的依赖,避免因外部资源而导致测试失败。

    • 功能测试:在功能测试初期,我们往往依赖到第三方提供的接口,但是第三方接口初期是不稳定的(虽然前期已做好服务接口的约定),后期当有较稳定的接口后,我们进行全面地测试,则接口的功能都正常了。如果数据准备比较麻烦或者造数据要双方配合成本比较高的项目,在对外部接口进行第一次全面联调测试后,仍然可以考虑使用mock来全面测试自己内部的功能逻辑。

    • UI自动化测试:对数据源进行mock,UI自动化测试时,页面上展示的信息是来源于其他的数据源。

    • 测试覆盖度,假如有一个接口,有100个不同类型的返回,我们需要测试它在不同返回类型下,系统是否能够正常响应,但是有些返回在正常情况下基本不会发生,难道你要千方百计给系统做各种手脚让它返回以便测试吗?比如,我们需要测试在当接口发生500错误的时候,app是否崩溃,别告诉我你一定要给服务端代码做些手脚让它返回500 。而使用mock,这一切就都好办了,想要什么返回就模拟什么返回,妈妈再也不用担心我的测试覆盖度了!

Mock框架对比(JAVA)

框架简介说明
EasyMockEasyMock 是早期比较流行的MocK测试框架。它提供对接口的模拟,能够通过录制、回放、检查三步来完成大体的测试过程,可以验证方法的调用种类、次数、顺序,可以令 Mock 对象返回指定的值或抛出指定异常。通过 EasyMock,我们可以方便的构造 Mock 对象从而使单元测试顺利进行。
MockitoEasyMock之后流行的mock工具。它与EasyMock和jMock很相似,但是通过在执行校验什么已经被调用,它消除了对期望行为(expectations)的需要。其它的mocking库需要你在执行记录期望行为(expectations),而这导致了丑陋的初始化代码。相对EasyMock学习成本低,而且具有非常简洁的API,测试代码的可读性很高。
PowerMock这个工具是在EasyMock和Mockito上扩展出来的,目的是为了解决EasyMock和Mockito不能解决的问题,比如对static, final, private方法均不能mock。其实测试架构设计良好的代码,一般并不需要这些功能,但如果是在已有项目上增加单元测试,老代码有问题且不能改时,就不得不使用这些功能了。PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能。目前PowerMock 仅扩展了 EasyMock 和 mockito,需要和EasyMock或Mockito配合一起使用。
JmockitJMockit 是一个轻量级的mock框架是用以帮助开发人员编写测试程序的一组工具和API,该项目完全基于 Java 5 SE 的 java.lang.instrument 包开发,内部使用 ASM 库来修改Java的Bytecode。Jmockit功能和PowerMock类似,某些功能甚至更为强大,但其代码的可读性并不强。
MockMVC基于RESTful风格的SpringMVC单元测试,可以测试完整的SpringMVC流程,即从URL请求到控制处理器,到视图渲染都可以测试。

综合了各个工具的特性及资料丰富程度,我建议大家使用Mockito,简单实用,文档丰富。下面就Mockito这个框架进行展开说明。


二、Mockito研究

主要资源

  • 官网:http://mockito.org

  • API文档:http://docs.mockito.googlecode.com/hg/org/mockito/Mockito.html

  • 项目源码:https://github.com/mockito/mockito ,Current version is 3.x

环境起步

通过Maven管理的,需要在项目的Pom.xml中增加如下的依赖:

<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>3.5.9</version>
  <scope>test</scope>
</dependency>

需要配合junit使用

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

核心注解

  • @RunWith

    这是一个类级别的注释。它用于保持测试干净并改善调试。它还会检测测试中可用的未使用的存根, 并使用@Mock注释对模拟进行初始化。@RunWith批注在org.mockito.junit包中可用。
    以下代码段显示了如何使用@RunWith批注:

    @RunWith(MockitoJUnitRunner.class)
    public class ToDoBusinessMock {
    .....
    }
  • @Captor

    它允许创建字段级参数捕获器。它与Mockito的verify()方法一起使用, 以获取调用方法时传递的值。

    @Captor
    ArgumentCaptor<String> argumentCaptor;
  • @InjectMocks

    创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。

    @InjectMocks
    ToDoService servicemock;

    注:ToDoService 不能是接口,而是一个具体实现类。
  • @Mock

    Mock 对函数的调用均执行mock(即虚假函数),不执行真正部分。

    @Mock
    ToDoService servicemock;
  • @Spy

    Spy对函数的调用均执行真正部分。

    @Spy
    ToDoService servicemock;

常用方法

  • mock一个对象

    List list = mock(List.class);
  • 验证交互行为

      静态导入会使代码更简洁
    import static org.mockito.Mockito.*;

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

    using mock object 使用mock对象
    mockedList.add("one");
    mockedList.clear();

    verification 验证
    verify(mockedList).add("one");
    verify(mockedList).clear();
  • 模拟期望结果

      List list = mock(List.class);
    when(mock.get(0)).thenReturn("hello");
    assertThat(mock.get(0),is("hello"));
  • 模拟抛出异常

      List list = mock(List.class);
    模拟行为
    doThrow(new RuntimeException()).when(list).add(1);
    执行时抛出异常
    list.add(1);
  • 为程序打桩(stub)

     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));
  • 参数匹配器

      使用内置的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());
  • 验证函数的确切、最少、从未调用次数

    //using mock
    mockedList.add("once");

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

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

    下面的两个验证函数效果一样,因为verify默认验证的就是times(1)
    verify(mockedList).add("once");
    verify(mockedList, times(1)).add("once");

    验证具体的执行次数
    verify(mockedList, times(2)).add("twice");
    verify(mockedList, times(3)).add("three times");

    使用never()进行验证,never相当于times(0)
    verify(mockedList, never()).add("never happened");

    使用atLeast()/atMost()
    verify(mockedList, atLeastOnce()).add("three times");
    verify(mockedList, atLeast(2)).add("five times");
    verify(mockedList, atMost(5)).add("three times");
  • 验证执行顺序

      A. 验证mock一个对象的函数执行顺序
    List singleMock = mock(List.class);
    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);

    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");
  • 验证零互动

     @Test
    public void mockTest(){
      List list = mock(List.class);
      List list2 = mock(List.class);
      List list3 = mock(List.class);
      list.add(1);
      verify(list).add(1);
      verify(list,never()).add(2);
      验证零互动行为
      verifyZeroInteractions(list2,list3);
    }
  • 验证冗余互动行为

    @Test(expected = NoInteractionsWanted.class)
    public void mockTest(){
    List list = mock(List.class);
    list.add(1);
    list.add(2);
    verify(list,times(2)).add(anyInt());
    检查是否有未被验证的互动行为,因为add(1)和add(2)都会被上面的anyInt()验证到
    所以下面的代码会通过
    verifyNoMoreInteractions(list);

    List list2 = mock(List.class);
    list2.add(1);
    list2.add(2);
    verify(list2).add(1);
    检查是否有未被验证的互动行为,因为add(2)没有被验证,所以下面的代码会失败抛出异常
    verifyNoMoreInteractions(list2);
    }
  • 为回调做测试桩

     运行为泛型接口Answer打桩

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


    输出 : "called with arguments: foo"
    System.out.println(mock.someMethod("foo"));
  • 重置mock

      List list = mock(List.class);
    when(list.size()).thenReturn(10);
     
    list.add(1);
    assertThat(list.size(),is(10));
     
    重置mock,清除所有的互动和预设
    reset(list);
    assertThat(list.size(),is(0));

三、MockMVC研究

使用目的

       对模块进行集成测试时,希望能够通过输入URL对Controller进行测试,如果通过启动服务器,建立http client进行测试,这样会使得测试变得很麻烦,比如,启动速度慢,测试验证不方便,依赖网络环境等,所以为了可以对Controller进行测试,可以引入了MockMVC。

环境起步

spring-test项目中带有mockmvc相关类,pom.xml增加依赖

 <dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <version>${spring.version}</version>
  <scope>test</scope>  
</dependency>

运行配置

  • RunWith(SpringJUnit4ClassRunner.class): 表示使用Spring Test组件进行单元测试。

  • @WebAppConfiguration: 使用这个Annotate会在跑单元测试的时候真实的启一个web服务,然后开始调用Controller的Rest API,待单元测试跑完之后再将web服务停掉;

  • @ContextConfiguration: 指定Bean的配置文件信息,可以有多种方式,这个例子使用的是文件路径形式,如果有多个配置文件,可以将括号中的信息配置为一个字符串数组来表示。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath*:**web-config.xml")
@WebAppConfiguration
public class MockMvcTest {
  ……
}

运行机制

  • MockMvcBuilder构造MockMvc的构造器

  • mockMvc调用perform,执行一个RequestBuilder请求,调用controller的业务处理逻辑

  • perform返回ResultActions,返回操作结果,通过ResultActions,提供了统一的验证方式

  • 使用StatusResultMatchers对请求结果进行验证

  • 使用ContentResultMatchers对请求返回的内容进行验证

创建Builder

有两种创建方式:

  • standaloneSetup(Object... controllers): 通过参数指定一组控制器,这样就不需要从上下文获取了。

    @Before
    public void setUp() throws Exception {
        mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();
    }

    这种测试方法的优势是:

    • 不依赖Spring容器,启动和测试耗时较少;

    • 集中于Controller层逻辑,不受其他服务接口的影响。

  • webAppContextSetup(WebApplicationContext wac):指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc

     @Before
    public void setup() {
          mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

方法解析

@Test
public void getAllCategoryTest() throws Exception {
      String responseString = mockMvc.perform(
                      get("/categories/getAllCategory")       请求的url,请求的方法是get
                      .contentType(MediaType.APPLICATION_FORM_URLENCODED) 数据的格式
               .param("pcode","root"))                             添加参数
              .andExpect(status().isOk())   返回的状态是200
              .andDo(print())         打印出请求和相应的内容
              .andReturn().getResponse().getContentAsString();   将相应的数据转换为字符串
      System.out.println("--------返回的json = " + responseString);
}
  • perform执行一个RequestBuilder请求,会自动执行SpringMVC的流程并映射到相应的控制器执行处理;

  • get:声明发送一个get请求的方法。MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables):根据uri模板和uri变量值得到一个GET请求方式的。另外提供了其他的请求的方法,如:post、put、delete等。

  • param:添加request的参数,如上面发送请求的时候带上了了pcode = root的参数。假如使用需要发送json数据格式的时将不能使用这种方式,可见后面被@ResponseBody注解参数的解决方法

  • andExpect:添加ResultMatcher验证规则,验证控制器执行完成结果是否正确(对返回的数据进行的判断);

  • andDo:添加ResultHandler结果处理器,比如调试时打印结果到控制台(对返回的数据进行的判断);

  • andReturn:最后返回相应的MvcResult;然后进行自定义验证/进行下一步的异步处理(对返回的数据进行的判断);


四、Mock实例

MockMVC实例

  • application/x-www-form-urlencoded

这是比较常用的提交数据的方式,在项目中也使用的是这一种,在MockMvc测试的参数准备时可以这样使用:

UrlEncodedFormEntity form = new UrlEncodedFormEntity(Arrays.asList(
              new BasicNameValuePair("参数1", value1),
              new BasicNameValuePair("参数2", value2),
      ), "utf-8");
MvcResult result = mvc.perform(MockMvcRequestBuilders.post("/test")
      .contentType(MediaType.APPLICATION_FORM_URLENCODED)
      .content(EntityUtils.toString(form)))
      .andExpect(status().isOk())
      .andReturn();
  • application/json

我使用的是用阿里开源的fastjson直接转换的形式:

Parm parm = new Parm(); 你要传的参数对象
//设置参数对象的值
String requestJson = JSONObject.toJSONString(parm);
MvcResult result = mockMvc.perform(post("/softs")
      .contentType(MediaType.APPLICATION_JSON)
      .content(requestJson))
      .andDo(print())
      .andExpect(status().isOk())
      .andReturn();
  • multipart/form-data

MockMultipartFile file = new MockMultipartFile("data", "dummy.csv",
      "text/plain", "Some dataset...".getBytes());

MockMultipartHttpServletRequestBuilder builder =
      MockMvcRequestBuilders.fileUpload("/test1/datasets/set1");
builder.with(new RequestPostProcessor() {
      public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) {
          request.setMethod("PUT");
          return request;
      }
  });
mvc.perform(builder.file(file))
      .andExpect(status().ok());

来一个完整的例子:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:config/IncotermsRestServiceTest-context.xml")
@WebAppConfiguration
public class TestApiOne {

  private MockMvc mockMvc;

  @Before
  public void setUp() {
      APIController apiController = new APIController();
      mockMvc = MockMvcBuilders.standaloneSetup(apiController).build();
  }

  @Test
  public void testGetSequence() {
      try {
          MvcResult mvcResult =           mockMvc.perform(MockMvcRequestBuilders.post("/api/getSequence"))
                              .andExpect(MockMvcResultMatchers.status().is(200))
                              .andDo(MockMvcResultHandlers.print())
                              .andReturn();
          int status = mvcResult.getResponse().getStatus();
          System.out.println("请求状态码:" + status);
          String result = mvcResult.getResponse().getContentAsString();
          System.out.println("接口返回结果:" + result);
          JSONObject resultObj = JSON.parseObject(result);
          // 判断接口返回json中success字段是否为true
          Assert.assertTrue(resultObj.getBooleanValue("success"));
      } catch (Exception e) {
          e.printStackTrace();
      }
  }
}

Mockito实例

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:config/IncotermsRestServiceTest-context.xml")
@WebAppConfiguration
public class FinanceServiceTest{

  @InjectMocks
  FinanceService service;

  @Mock
  FinanceMapper mapper;

  @Before
  public void setUp() {  
      MockitoAnnotations.initMocks(this);
  }  

  @Test
  public void selectByExampleTest() {
      Finance f = Mockito.mock(Finance.class);;
      f.setBak("bak");
      f.setConsumer("consumer");
      String id = "1";

      Mockito.when(mapper.searchFinanceById(id)).thenReturn(f);
      Finance returnF = service.searchFinanceById(id);
      Mockito.verify(mapper).searchFinanceById(id);

      Assert.assertEquals(f.getConsumer(), returnF.getConsumer());

  }
}


附录:参考文档一览

Mockito官网:http://site.mockito.org/

5分钟了解Mockito:http://liuzhijun.iteye.com/blog/1512780

Mockito简单介绍及示例:http://blog.csdn.net/huoshuxiao/article/details/6107835

Mockito浅谈:http://www.jianshu.com/p/77db26b4fb54

单元测试利器-Mockito 中文文档:http://blog.csdn.net/bboyfeiyu/article/details/52127551

Mockito使用指南 :http://blog.csdn.net/shensky711/article/details/52771493

JUnit+Mockito 单元测试(二):http://blog.csdn.net/zhangxin09/article/details/42422643

MockMvc和Mockito之酷炫使用 :https://www.cnblogs.com/treerain/p/mockmvc_mockito.html




文章转载自Jerry分享汇,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论