JUnit+Mockito测试REST API时testGetProductDetails用例失败排查
API的JUnit测试用例编写
我是JUnit和Mockito的初学者,正尝试为controller类编写单元测试用例,实现API接口逻辑验证。
待测试的Controller类代码
package com.mylearnings.controller; import com.mylearnings.modal.Product; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @RestController public class ProductController { private HashMap<String, Product> productCatalog = new HashMap<>(); @PostMapping("/product") public ResponseEntity addProduct(@RequestBody Product product) { productCatalog.put(product.getId(), product); return new ResponseEntity("product added successfully", HttpStatus.CREATED); } @GetMapping("/product/{id}") public ResponseEntity getProductDetails(@PathVariable String id) { return ResponseEntity.ok(productCatalog.get(id)); } @GetMapping("/product") public List<Product> getProductList() { return new ArrayList<>(productCatalog.values()); } @PutMapping("/product") public String updateProduct(@RequestBody Product product) { productCatalog.put(product.getId(), product); return "product updated successfully"; } @DeleteMapping("/product/{id}") public String deleteProduct(@PathVariable String id) { productCatalog.remove(id); return "product deleted successfully"; } }
已尝试的实现
添加了@ExtendWith(MockitoExtension.class)注解进行尝试,但测试依然失败,编写的测试类代码如下:
package com.mylearnings.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.mylearnings.modal.Product; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.web.context.WebApplicationContext; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @SpringBootTest @AutoConfigureMockMvc public class ProductControllerTest { @Autowired private MockMvc mockMvc; @Autowired private WebApplicationContext webApplicationContext; @Mock private Map<String, Product> productCatalog; @InjectMocks private ProductController productController; @Test public void testAddProduct() throws Exception { MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/product").contentType(MediaType.APPLICATION_JSON).content(new ObjectMapper().writeValueAsString(new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d)))).andReturn(); assertEquals(201, mvcResult.getResponse().getStatus()); } @Test public void testGetProductDetails() throws Exception { Product product = new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d); when(productCatalog.get("MS116")).thenReturn(product); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/product/{id}", "MS116").accept(MediaType.APPLICATION_JSON)).andReturn(); assertEquals(200, mvcResult.getResponse().getStatus()); Product result = new ObjectMapper().readValue(mvcResult.getResponse().getContentAsString(), Product.class); assertEquals(product, result); } }
问题现象
当前testGetProductDetails()测试用例执行失败,无法确定故障原因是否与Map类型依赖的Mock逻辑有关。
故障原因
测试失败核心是两个配置错误:
- 测试逻辑混用:同时使用
@SpringBootTest加载完整Spring上下文、@Mock+@InjectMocks手动生成Mock实例,两套逻辑完全独立。@SpringBootTest启动时会自行创建ProductController实例,使用类内部初始化的真实HashMap,不会注入手动构造的带Mock的Controller对象,因此写的Mock存根逻辑根本不会触发。 - Mock类型不匹配:Controller中
productCatalog的声明类型是HashMap<String, Product>,测试中Mock的是Map<String, Product>接口类型,即使单独使用Mockito测试,类型不匹配也会导致依赖注入失败。
解决方案
根据测试目标二选一即可:
方案1:Controller层纯单元测试(无Spring上下文,执行速度快)
移除Spring测试相关注解,只用Mockito独立搭建MockMvc环境,直接验证Controller逻辑,修正后的测试代码:
package com.mylearnings.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.mylearnings.modal.Product; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import java.util.HashMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ProductControllerTest { private MockMvc mockMvc; @Mock // Mock类型和Controller声明保持一致,使用HashMap private HashMap<String, Product> productCatalog; @InjectMocks private ProductController productController; private final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach void setUp() { // 手动绑定Controller搭建MockMvc,不启动Spring容器 mockMvc = MockMvcBuilders.standaloneSetup(productController).build(); } @Test public void testAddProduct() throws Exception { Product testProduct = new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/product") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testProduct))) .andReturn(); assertEquals(201, mvcResult.getResponse().getStatus()); } @Test public void testGetProductDetails() throws Exception { Product product = new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d); when(productCatalog.get("MS116")).thenReturn(product); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/product/{id}", "MS116") .accept(MediaType.APPLICATION_JSON)) .andReturn(); assertEquals(200, mvcResult.getResponse().getStatus()); Product result = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), Product.class); assertEquals(product, result); } }
方案2:接口集成测试(启动Spring上下文,验证全链路逻辑)
如果要做集成测试,不需要手动Mock Controller内部的productCatalog,直接按照接口真实调用流程测试即可:先调用新增接口插入测试数据,再调用查询接口验证返回结果,修正后的测试代码:
package com.mylearnings.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.mylearnings.modal.Product; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest @AutoConfigureMockMvc public class ProductControllerTest { @Autowired private MockMvc mockMvc; private final ObjectMapper objectMapper = new ObjectMapper(); @Test public void testAddProduct() throws Exception { Product testProduct = new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d); MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/product") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testProduct))) .andReturn(); assertEquals(201, mvcResult.getResponse().getStatus()); } @Test public void testGetProductDetails() throws Exception { // 先插入测试数据 Product product = new Product("MS116", "Dell MS116", "Dell MS116 Usb wired optical mouse", "", 229d); mockMvc.perform(MockMvcRequestBuilders.post("/product") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(product))) .andReturn(); // 执行查询并验证 MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.get("/product/{id}", "MS116") .accept(MediaType.APPLICATION_JSON)) .andReturn(); assertEquals(200, mvcResult.getResponse().getStatus()); Product result = objectMapper.readValue(mvcResult.getResponse().getContentAsString(), Product.class); assertEquals(product.getId(), result.getId()); assertEquals(product.getName(), result.getName()); } }
注:由于当前Controller中
productCatalog是Controller实例内部维护的内存存储,每次重启应用/新建Controller实例数据都会清空,集成测试如果存在用例执行顺序干扰,可以在每个测试执行后添加清空数据的逻辑。
内容的提问来源于stack exchange,提问作者vishal
相关产品推荐
相关产品推荐

