You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.03 07:51:27