JUnit 与 Mockito 组合测试 Controller 层-入门

发布于:2025-03-13 ⋅ 阅读:(18) ⋅ 点赞:(0)

为什么要组合Mockito ?

答:隔离 Controller 层,保证单元测试纯粹性

具体来说:

  • Controller 层通常依赖 Service、Repository 等组件,而这些组件可能涉及数据库、外部 API 或复杂逻辑,如果我们单纯使用JUnit ,我们实际上进行的就变成了集成测试
  • Mockito 允许我们模拟(Mock)这些依赖,避免真实调用数据库或外部服务,使测试更快速、稳定。

如何实现?

导入依赖

以maven为例,导入的依赖为:

<!--Spring Boot 默认使用 JUnit 作为主要的测试框架,并集成了 Mockito 作为 Mock 测试工具。-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!--用于测试运行器(如 IDE 或 Maven Surefire 插件)加载和执行 JUnit 测试-->
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <scope>test</scope>
</dependency>

新建测试类

被测类:

@RestController
@RequestMapping("/api")
public class MyController {

    private final MyService myService;

    @Autowired
    public MyController(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/data/{id}")
    public Result<String> getSomething(@PathVariable int id) {
        try {
            String data = myService.getDataById(id);
            if (data == null) {
                return Result.fail("Data not found");
            }
            return Result.ok(data);
        } catch (Exception e) {
            return Result.fail("Server error");
        }
    }
}



测试类:

class MyControllerTest {

    @Test
    void testGetSomething() {
    }
}

类注解

//启动完整的 Spring Boot 容器
@SpringBootTest 
// 自动配置 MockMvc,允许在不真正启动 Web 服务器的情况下,模拟 HTTP 请求
@AutoConfigureMockMvc 
// 扩展 JUnit 5,使其支持 Spring Test 框架。
@ExtendWith(SpringExtension.class)
class MyControllerTest {

	// 自动注入 MockMvc
    @Autowired
    private MockMvc mockMvc; 
	
	// Mock Service 层,避免真实调用数据库
    @MockBean
    private MyService myService; 

    @Test
    void testGetSomething() throws Exception {
        
    }
}

方法逻辑

1. 定义替换逻辑
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService; // Mock Service,避免真实调用

    @Test
    void testGetSomething_Success() throws Exception {
    	// 解析见下文
        when(myService.getDataById(anyInt())).thenReturn(Result.success("test001"));

       
    }

  
}

when(myService.getDataById(anyInt())).thenReturn("Data for ID 1");

  • when(myService.getDataById(...)) 用于指定 mock 行为,表示当 myService.getDataById(id) 被调用时,应该返回特定值。
  • anyInt()Mockito 的参数匹配器,表示无论传入的 id 是什么整数值,都返回 Result.success("test001")
  • thenReturn(Result.success("test001")) 指定返回的模拟数据。
2. 定义模拟操作
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService; // Mock Service,避免真实调用

    @Test
    void testGetSomething_Success() throws Exception {
    
        when(myService.getDataById(anyInt())).thenReturn(Result.success("test001"));
		
		//模拟
        mockMvc.perform(get("/api/data/1"))
                .andExpect(status().isOk())  
                .andExpect(jsonPath("$.data").isNotEmpty());
    }

  
}


mockMvc.perform(get("/api/data/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data").isNotEmpty())

  • mockMvc.perform(...) 执行一个模拟的 HTTP 请求,这里是 GET请求,目标 URL 为 /api/data/1
  • 这个请求会调用 MyController的getSomething(int id) 方法,模拟客户端访问 /api/data/1
  • andExpect(status().isOk()) :断言(Assertion)HTTP 响应状态码为 200 OK
  • andExpect(jsonPath("$.data").isNotEmpty()):假设Result结构是通用的code、msg和data,此处简单的测试一下有返回结果。

.andExpect(...) 方法简述

  • status().isOk() - 验证 HTTP 状态码
  • content().string("text") - 验证响应体(字符串)
  • content().json("{...}") - 验证 JSON 结构
  • header().string("Content-Type", "application/json") - 验证 HTTP 头
  • jsonPath("$.id").value(1) - 验证 JSON 字段
  • cookie().exists("SESSIONID") - 验证 Cookie
  • view().name("home") - 验证返回的视图名称(Spring MVC)

3. 确保执行
@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService; // Mock Service,避免真实调用

    @Test
    void testGetSomething_Success() throws Exception {
    
        when(myService.getDataById(anyInt())).thenReturn(Result.success("test001"));
		
        mockMvc.perform(get("/api/data/1"))
                .andExpect(status().isOk())  
                .andExpect(jsonPath("$.data").isNotEmpty());
        //确保执行过
        verfy(myService).getDataById(anyInt());
    }

}

verfy(myService).getDataById(anyInt());

  • 检查 myService.getDataById(int) 方法是否被调用过。
  • anyInt() 代表不关心具体传入的整数值,只要 getDataById() 被调用了就算测试通过。
    拓展:
	verify(myService, never()).getDataById(1);  // 确保从未调用
	verify(myService, times(2)).getDataById(1); // 必须调用 2 次
	verify(myService, atLeast(1)).getDataById(1); // 至少 1 次
	verify(myService, atMost(3)).getDataById(1); // 最多 3 次

完整示例

@SpringBootTest
@AutoConfigureMockMvc
@ExtendWith(SpringExtension.class)
class MyControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MyService myService; // Mock Service,避免真实数据库调用

    @Test
    void testGetSomething_Success() throws Exception {
         when(myService.getDataById(1)).thenReturn(Result.success("test001"));
		
        mockMvc.perform(get("/api/data/1"))
                .andExpect(status().isOk())  
                .andExpect(jsonPath("$.code").value(Result.success().getCode()))
                .andExpect(jsonPath("$.data").isNotEmpty())
                .andExpect(jsonPath("$.data").value("test001"));
        //确保执行过
        verfy(myService).getDataById(1);
    }

    @Test
    void testGetSomething_NotFound() throws Exception {
        when(myService.getDataById(2)).thenReturn(Result.fail("error"));

        mockMvc.perform(get("/api/data/2"))
                .andExpect(status().isOk())  
                .andExpect(jsonPath("$.code").value(Result.fail("").getCode()))
                .andExpect(jsonPath("$.msg").isNotEmpty());
		//确保执行过
        verfy(myService).getDataById(2);
    }

}