Mock测试详细介绍及使用

本文详细介绍了Mockito在Java单元测试中的使用,包括Mockito的定义、Spring Boot测试支持、Mockito工具的特性,以及Mock与Stub的区别。通过具体的代码示例展示了如何创建和使用Mock对象,以及在Controller层的测试应用。文章强调了测试数据准备的重要性,并给出了测试方法的命名建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在写一个微服务的项目,项目中使用了Mock。Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。

定义

1.什么是Mock
mock是在测试过程中,对于一些不容易构造/获取的对象,创建一个mock对象来模拟对象的行为。比如说你需要调用B服务,可是B服务还没有开发完成,那么你就可以将调用B服务的那部分给Mock掉,并编写你想要的返回结果。

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

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

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

3.Mockito工具
Mockito 是 Java 单元测试 Mock 框架,开源。

大多 Java Mock 库如 EasyMock 或 JMock 都是 expect-run-verify (期望-运行-验证)方式,而 Mockito 则使用更简单,更直观的方法:在执行后的互动中提问。使用 Mockito,你可以验证任何你想要的。而那些使用 expect-run-verify 方式的库,你常常被迫查看无关的交互。

非 expect-run-verify 方式 也意味着,Mockito 无需准备昂贵的前期启动。他们的目标是让开发人员专注于测试选定的行为。

Mockito 拥有的非常少的 API,所有开始使用 Mockito,几乎没有时间成本。因为只有一种创造 mock 的方式。只要记住,在执行前 stub,而后在交互中验证。你很快就会发现这样的TDD java 代码是多么自然。

类似 EasyMock 的语法来的,所以你可以放心地重构。Mockito 并不需要“expectation(期望)”的概念。只有 stub 和verify。

Mockito 实现了 Gerard Meszaros 所谓的 Test Spy.

项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能启动那些服务,这里就可以利用Mockito工具,其中有如下三种注解:

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

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

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

Mock 与 Stub 的区别

Mock 不是 Stub,两者是有区别的:

前者被称为 mockist TDD,而后者一般称为 classic TDD ;
前者是基于行为的验证(behavior verification),后者是基于状态的验证 (state verification);
前者使用的是模拟的对象,而后者使用的是真实的对象。

所谓打桩Stub,就是用来提供测试时所需要的测试数据,因为是mock的对象,所以可能有些方法并不能知道返回值,因此我们需要去假定返回值。可以对各种交互设置相应的回应,即对方法设置调用返回值,使用when(…).thenReturn(…)。

4. 常用的 Mockito 方法:
Mockito的使用,一般有以下几种组合:

  • do/when:包括doThrow(…).when(…)/doReturn(…).when(…)/doAnswer(…).when(…)

  • given/will:包括
    given(…).willReturn(…)/given(…).willAnswer(…)
    例如:given(userRepository.findByUserName(Mockito.anyString())).willReturn(user);
    given用于对指定方法进行返回值的定制,它需要与will开头的方法一起使用
    通过willReturn可以直接指定打桩的方法的返回值

  • when/then: 包括when(…).thenReturn(…)/when(…).thenAnswer(…)
    例如:when(userRepository.findByUserName(Mockito.anyString())).thenReturn(user);
    when的作用与Given有点类似,但它一般与then开头的方法一起使用。
    thenReturn与willReturn类似,不过它一般与when一起使用。

具体应用

以下一个具体例子:

/**
  * If an item is loaded from the repository, the name of that item should 
  * be transformed into uppercase.
  */
@Test
public void shouldReturnItemNameInUpperCase() {
 
    //
    // Given
    //
    Item mockedItem = new Item("it1", "Item 1", "This is item 1", 2000, true);
    when(itemRepository.findById("it1")).thenReturn(mockedItem);
 
    //
    // When
    //
    String result = itemService.getItemNameUpperCase("it1");
 
    //
    // Then
    //
    verify(itemRepository, times(1)).findById("it1");
    assertThat(result, is("ITEM 1"));

不同代码块的作用显而易见。不过我们还是总结下吧。

准备数据->Given
这个部分,创建我们Mock的函数的返回值,或者我们将要测试方法的输入参数。此外,mock的方法也会在这个部分中准备。通常单元测试Case中,这个部分应该是最长,也是最复杂的。

注意:Mockito的when声明其实是given部分的,这点需要说明下,因为的确容易困惑。但,这与测试执行的准备工作有关,因此,放在这里最为合适。

执行->When
这里一般只Call测试方法,这里标明了测试目的,因为这个部分的代码一般是最短的了。

验证->Then
这个部分,执行环节的所有结果在这里得以声明。除此之外,也可以确认方法是否被执行。总之,主要的点,在这里进行Check。

测试Case的命名(测试方法)
早期测试方法都是以test为前缀,现在已经不怎么这样做了,一些同事喜欢用下划线去命名,我个人喜欢用驼峰命名。

当然,一个方法命名需要包含大量的信息,但可能这些信息更好的方式是放在测试代码的注释上。表明这个方法会发生什么,像shouldReturnItemNameInUpperCase ()这样,可能是个好的办法,显而易见,在项目开始前,大家统一测试方法规范是好的。

Controller层的例子:
Controller层的单元测试比较简单,主要思路是mock业务层返回值,然后模拟调用接口。

Controller.java

@RestController
public class Controller {
	@Autowired
    private final IndiskService indiskService;
    private static final String AUTHORIZATION = "Authorization";
	
	@PostMapping("/ebs/{version}/volumes/{volume_id}/action/attach")
    public ReturnMsgVO attach(@PathVariable("volume_id") String volume_id,
                              @RequestBody JSONObject jsonObject,
                              HttpServletRequest request) {
        String keycloakToken = request.getHeader(AUTHORIZATION);
        AttachDetachExtendVO attachDetachExtendVO = new AttachDetachExtendVO();
		//do something......
		//正确返回200
        return indiskService.attachVolume(attachDetachExtendVO, request, keycloakToken);
    }

	@DeleteMapping("/ebs/{version}/volumes/{volume_id}/soft")
    public ReturnMsgVO softDeleteVolume(@PathVariable(value = "volume_id") String volumeId,
                                        @RequestBody String orderJson,
                                        HttpServletRequest request) {
		String keycloakToken = request.getHeader(AUTHORIZATION);
		//正确返回202,消息体为{"code":"202","message":null,"data":null}
        return indiskService.softDeleteVolume(volumeId, keycloakToken, orderJson);
    }
}

测试类

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

public class ControllerTest {

    private static final String AUTHORIZATION = "Authorization";

    private MockMvc mvc;

    @Mock
    //要mock被测类中依赖的对象使用@Mock注解
    private ServiceImpl serviceImpl;

    @InjectMocks
    //被测类本身使用@InjectMocks注解
    private Controller controller;

    @Before
    public void setup() {
    	//初始化
        MockitoAnnotations.initMocks(this);
        //构建mvc环境
        mvc = MockMvcBuilders.standaloneSetup(controller).build();
    }


    @Test
    public void testSoftDeleteVolume() throws Exception {
        //mock方法行为
        Mockito.when(serviceImpl.softDeleteVolume("1", "2", "3"))
                .thenReturn(ReturnMsgVO.builder().code("202").build());

		//模拟接口调用
        this.mvc.perform(delete("/ebs/v1/volumes/1/soft")
                .header(AUTHORIZATION, "2").content("3"))
                .andExpect(status().isOk())
                //对接口响应进行验证
                .andExpect(content().json("{\"code\":\"202\",\"message\":null,\"data\":null}"));
    }

    @Test
    public void attach() throws Exception {
        Mockito.when(serviceImpl.attachVolume(Mockito.any(), Mockito.any(), Mockito.anyString()))
                .thenReturn(ReturnMsgUtil.success());

        mvc.perform(post("/ebs/v1/volumes/123/action/attach")
                .header(AUTHORIZATION, "123")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"os-attach\":{\"instance_uuid\":\"123\"}}"))
                .andExpect(status().isOk());
    }
}

总结

为了去正确执行一个测试,有时为此准备大量的数据,是非常头疼的。尤其是如果在不同的测试Case,我们需要去Mock相同的一些方法或者数据,那么这个时候,我们做个类函数,去共享这些,这样会比较好,当然这些Mock对象因为影响着诸多的测试,所以也会越变越复杂,而且大量的测试这些对象。因此,在创建测试,Mock数据时,怎么做,如何做,我们需要权衡利弊,再三考虑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值