Spring Boot Rest Controller 怎么做单元测试?

文章导读
Previous Quiz Next Spring Boot 提供了一种简单的方法来为 Rest Controller 文件编写 Unit Test。通过 SpringJUnit4ClassRunner 和 MockMvc 的帮助,我们可以创建一个 web applicat
📋 目录
  1. 示例 - Rest Controller 的单元测试
  2. 为 REST Controller 编写单元测试
A A

Spring Boot - Rest Controller 单元测试



Previous
Quiz
Next

Spring Boot 提供了一种简单的方法来为 Rest Controller 文件编写 Unit Test。通过 SpringJUnit4ClassRunner 和 MockMvc 的帮助,我们可以创建一个 web application context 来为 Rest Controller 文件编写 Unit Test。

Unit Test 应该编写在 src/test/java 目录下,用于编写测试的 classpath resources 应该放置在 src/test/resources 目录下。

示例 - Rest Controller 的单元测试

在编写 Unit Test 时,我们需要在构建配置文件中添加 Spring Boot Starter Test 依赖,如下所示。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
</dependency>

Gradle 用户可以在 build.gradle 文件中添加以下依赖。

testCompile('org.springframework.boot:spring-boot-starter-test')

在编写测试用例之前,我们应该先构建 RESTful web services。有关构建 RESTful web services 的更多信息,请参阅构建 RESTful Web Services 章节。

为 REST Controller 编写单元测试

在本节中,我们将了解如何为 REST Controller 编写单元测试。

首先,我们需要创建一个 abstract class 文件,使用 MockMvc 创建 web application context,并定义 mapToJson() 和 mapFromJson() 方法,将 Java 对象转换为 JSON 字符串,以及将 JSON 字符串转换为 Java 对象。

AbstractTest.java

package com..demo;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@SpringBootTest(classes = DemoApplication.class)
@WebAppConfiguration
public abstract class AbstractTest {
   protected MockMvc mvc;
   @Autowired
   WebApplicationContext webApplicationContext;

   protected void setUp() {
      mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
   }
   protected String mapToJson(Object obj) throws JsonProcessingException {
      ObjectMapper objectMapper = new ObjectMapper();
      return objectMapper.writeValueAsString(obj);
   }
   protected <T> T mapFromJson(String json, Class<T> clazz)
      throws JsonParseException, JsonMappingException, IOException {
      
      ObjectMapper objectMapper = new ObjectMapper();
      return objectMapper.readValue(json, clazz);
   }
}

接下来,编写一个继承 AbstractTest 类的类文件,并为 GET、POST、PUT 和 DELETE 等每个方法编写单元测试。

以下是 GET API 测试用例的代码。此 API 用于查看产品列表。

@Test
public void getProductsList() throws Exception {
   String uri = "/products";
   MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get(uri)
      .accept(MediaType.APPLICATION_JSON_VALUE)).andReturn();
   
   int status = mvcResult.getResponse().getStatus();
   assertEquals(200, status);
   String content = mvcResult.getResponse().getContentAsString();
   Product[] productlist = super.mapFromJson(content, Product[].class);
   assertTrue(productlist.length > 0);
}

以下是 POST API 测试用例的代码。此 API 用于创建产品。

@Test
public void createProduct() throws Exception {
   String uri = "/products";
   Product product = new Product();
   product.setId("3");
   product.setName("Ginger");
   
   String inputJson = super.mapToJson(product);
   MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.post(uri)
      .contentType(MediaType.APPLICATION_JSON_VALUE).content(inputJson)).andReturn();
   
   int status = mvcResult.getResponse().getStatus();
   assertEquals(201, status);
   String content = mvcResult.getResponse().getContentAsString();
   assertEquals(content, "Product is created successfully");
}

以下是 PUT API 测试用例的代码。此 API 用于更新现有产品。

@Test
public void updateProduct() throws Exception {
   String uri = "/products/2";
   Product product = new Product();
   product.setName("Lemon");
   
   String inputJson = super.mapToJson(product);
   MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.put(uri)
      .contentType(MediaType.APPLICATION_JSON_VALUE).content(inputJson)).andReturn();
   
   int status = mvcResult.getResponse().getStatus();
   assertEquals(200, status);
   String content = mvcResult.getResponse().getContentAsString();
   assertEquals(content, "Product is updated successsfully");
}

以下是 Delete API 测试用例的代码。此 API 将删除现有产品。

@Test
public void deleteProduct() throws Exception {
   String uri = "/products/2";
   MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.delete(uri)).andReturn();
   int status = mvcResult.getResponse().getStatus();
   assertEquals(200, status);
   String content = mvcResult.getResponse().getContentAsString();
   assertEquals(content, "Product is deleted successsfully");
}

完整的 Controller 测试类文件如下 −

ProductServiceControllerTest.java

package com..demo;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import com..demo.model.Product;

public class ProductServiceControllerTest extends AbstractTest {
   @Override
   @BeforeEach
   public void setUp() {
      super.setUp();
   }
   @Test
   public void getProductsList() throws Exception {
      String uri = "/products";
      MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.get(uri)
         .accept(MediaType.APPLICATION_JSON_VALUE)).andReturn();
      
      int status = mvcResult.getResponse().getStatus();
      assertEquals(200, status);
      String content = mvcResult.getResponse().getContentAsString();
      Product[] productlist = super.mapFromJson(content, Product[].class);
      assertTrue(productlist.length > 0);
   }
   @Test
   public void createProduct() throws Exception {
      String uri = "/products";
      Product product = new Product();
      product.setId("3");
      product.setName("Ginger");
      String inputJson = super.mapToJson(product);
      MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.post(uri)
         .contentType(MediaType.APPLICATION_JSON_VALUE)
         .content(inputJson)).andReturn();
      
      int status = mvcResult.getResponse().getStatus();
      assertEquals(201, status);
      String content = mvcResult.getResponse().getContentAsString();
      assertEquals(content, "Product is created successfully");
   }
   @Test
   public void updateProduct() throws Exception {
      String uri = "/products/2";
      Product product = new Product();
      product.setName("Lemon");
      String inputJson = super.mapToJson(product);
      MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.put(uri)
         .contentType(MediaType.APPLICATION_JSON_VALUE)
         .content(inputJson)).andReturn();
      
      int status = mvcResult.getResponse().getStatus();
      assertEquals(200, status);
      String content = mvcResult.getResponse().getContentAsString();
      assertEquals(content, "Product is updated successsfully");
   }
   @Test
   public void deleteProduct() throws Exception {
      String uri = "/products/2";
      MvcResult mvcResult = mvc.perform(MockMvcRequestBuilders.delete(uri)).andReturn();
      int status = mvcResult.getResponse().getStatus();
      assertEquals(200, status);
      String content = mvcResult.getResponse().getContentAsString();
      assertEquals(content, "Product is deleted successsfully");
   }
}

完整的构建配置文件 Maven 构建 pom.xml 的代码如下 −

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>3.5.6</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.</groupId>
   <artifactId>demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>demo</name>
   <description>Demo project for Spring Boot</description>
   <url/>
   <licenses>
      <license/>
   </licenses>
   <developers>
      <developer/>
   </developers>
   <scm>
      <connection/>
      <developerConnection/>
      <tag/>
      <url/>
      </scm>
      <properties>
         <java.version>21</java.version>
      </properties>
      <dependencies>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
         </dependency>
      </dependencies>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>
</project>

编译和执行

您可以使用以下 Maven 或 Gradle 命令创建可执行 JAR 文件并运行 Spring Boot 应用程序 −

对于 Maven,可以使用以下命令 −

mvn clean install 

现在,您可以在控制台窗口中看到测试结果。

[INFO] Scanning for projects...
[INFO] 
[INFO] [1m----------------------< [0;36mcom.:demo[0;1m >-----------------------[m
[INFO] [1mBuilding demo 0.0.1-SNAPSHOT[m
[INFO]   from pom.xml
[INFO] [1m--------------------------------[ war ]---------------------------------[m
[INFO] 
[INFO] [1m--- [0;32mresources:3.3.1:resources[m [1m(default-resources)[m @ [36mdemo[0;1m ---[m
[INFO] Copying 1 resource from src\main\resources to target\classes
[INFO] Copying 8 resources from src\main\resources to target\classes
[INFO] 
[INFO] [1m--- [0;32mcompiler:3.14.0:compile[m [1m(default-compile)[m @ [36mdemo[0;1m ---[m
[INFO] Nothing to compile - all classes are up to date.
[INFO] 
[INFO] [1m--- [0;32mresources:3.3.1:testResources[m [1m(default-testResources)[m @ [36mdemo[0;1m ---[m
[INFO] skip non existing resourceDirectory D:\Projects\demo\src\test\resources
[INFO] 
[INFO] [1m--- [0;32mcompiler:3.14.0:testCompile[m [1m(default-testCompile)[m @ [36mdemo[0;1m ---[m
[INFO] Nothing to compile - all classes are up to date.
[INFO] 
[INFO] [1m--- [0;32msurefire:3.5.4:test[m [1m(default-test)[m @ [36mdemo[0;1m ---[m
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com..demo.[1mDemoApplicationTests[m
[2025-09-30T12:09:38Z] [org.springframework.test.context.support.AnnotationConfigContextLoaderUtils] [main] [83] [INFO ] Could not detect default configuration classes for test class [com..demo.DemoApplicationTests]: DemoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
[2025-09-30T12:09:38Z] [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] [main] [234] [INFO ] Found @SpringBootConfiguration com..demo.DemoApplication for test class com..demo.DemoApplicationTests

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.6)

[2025-09-30T12:09:38Z] [org.springframework.boot.StartupInfoLogger] [main] [53] [INFO ] Starting DemoApplicationTests using Java 21.0.6 with PID 38544 (started by mahes in D:\Projects\demo)
[2025-09-30T12:09:38Z] [org.springframework.boot.SpringApplication] [main] [652] [INFO ] No active profile set, falling back to 1 default profile: "default"
[2025-09-30T12:09:40Z] [org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping] [main] [59] [INFO ] Adding welcome page template: index
[2025-09-30T12:09:41Z] [org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver] [main] [60] [INFO ] Exposing 1 endpoint beneath base path '/actuator'
[2025-09-30T12:09:41Z] [org.springframework.boot.StartupInfoLogger] [main] [59] [INFO ] Started DemoApplicationTests in 2.571 seconds (process running for 3.792)
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
WARNING: A Java agent has been loaded dynamically (C:\Users\mahes\.m2\repository\net\bytebuddy\byte-buddy-agent\1.17.7\byte-buddy-agent-1.17.7.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
[INFO] [1;32mTests run: [0;1;32m1[m, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.134 s -- in com..demo.[1mDemoApplicationTests[m
[INFO] Running com..demo.[1mProductServiceControllerTest[m

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.5.6)

[2025-09-30T12:09:42Z] [org.springframework.boot.StartupInfoLogger] [main] [53] [INFO ] Starting ProductServiceControllerTest using Java 21.0.6 with PID 38544 (started by mahes in D:\Projects\demo)
[2025-09-30T12:09:42Z] [org.springframework.boot.SpringApplication] [main] [652] [INFO ] No active profile set, falling back to 1 default profile: "default"
[2025-09-30T12:09:42Z] [org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping] [main] [59] [INFO ] Adding welcome page template: index
[2025-09-30T12:09:42Z] [org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver] [main] [60] [INFO ] Exposing 1 endpoint beneath base path '/actuator'
[2025-09-30T12:09:42Z] [org.springframework.boot.StartupInfoLogger] [main] [59] [INFO ] Started ProductServiceControllerTest in 0.472 seconds (process running for 5.203)
[2025-09-30T12:09:42Z] [org.springframework.mock.web.MockServletContext] [main] [440] [INFO ] Initializing Spring TestDispatcherServlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [532] [INFO ] Initializing Servlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [554] [INFO ] Completed initialization in 1 ms
Pre Handle method is Calling
Post Handle method is Calling
Request and Response is completed
[2025-09-30T12:09:42Z] [org.springframework.mock.web.MockServletContext] [main] [440] [INFO ] Initializing Spring TestDispatcherServlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [532] [INFO ] Initializing Servlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [554] [INFO ] Completed initialization in 1 ms
Pre Handle method is Calling
Post Handle method is Calling
Request and Response is completed
[2025-09-30T12:09:42Z] [org.springframework.mock.web.MockServletContext] [main] [440] [INFO ] Initializing Spring TestDispatcherServlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [532] [INFO ] Initializing Servlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [554] [INFO ] Completed initialization in 1 ms
Pre Handle method is Calling
Post Handle method is Calling
Request and Response is completed
[2025-09-30T12:09:42Z] [org.springframework.mock.web.MockServletContext] [main] [440] [INFO ] Initializing Spring TestDispatcherServlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [532] [INFO ] Initializing Servlet ''
[2025-09-30T12:09:42Z] [org.springframework.web.servlet.FrameworkServlet] [main] [554] [INFO ] Completed initialization in 1 ms
Pre Handle method is Calling
Post Handle method is Calling
Request and Response is completed
[INFO] [1;32mTests run: [0;1;32m4[m, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.773 s -- in com..demo.[1mProductServiceControllerTest[m
[INFO] 
[INFO] Results:
[INFO] 
[INFO] [1;32mTests run: 5, Failures: 0, Errors: 0, Skipped: 0[m
[INFO] 
[INFO] 
[INFO] [1m--- [0;32mwar:3.4.0:war[m [1m(default-war)[m @ [36mdemo[0;1m ---[m
[INFO] Packaging webapp
[INFO] Assembling webapp [demo] in [D:\Projects\demo\target\demo-0.0.1-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources [D:\Projects\demo\src\main\webapp]
[INFO] Building war: D:\Projects\demo\target\demo-0.0.1-SNAPSHOT.war
[INFO] 
[INFO] [1m--- [0;32mspring-boot:3.5.6:repackage[m [1m(repackage)[m @ [36mdemo[0;1m ---[m
[INFO] Replacing main artifact D:\Projects\demo\target\demo-0.0.1-SNAPSHOT.war with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to D:\Projects\demo\target\demo-0.0.1-SNAPSHOT.war.original
[INFO] 
[INFO] [1m--- [0;32minstall:3.1.4:install[m [1m(default-install)[m @ [36mdemo[0;1m ---[m
[INFO] Installing D:\Projects\demo\pom.xml to C:\Users\mahes\.m2\repository\com\\demo\0.0.1-SNAPSHOT\demo-0.0.1-SNAPSHOT.pom
[INFO] Installing D:\Projects\demo\target\demo-0.0.1-SNAPSHOT.war to C:\Users\mahes\.m2\repository\com\\demo\0.0.1-SNAPSHOT\demo-0.0.1-SNAPSHOT.war
[INFO] [1m------------------------------------------------------------------------[m
[INFO] [1;32mBUILD SUCCESS[m
[INFO] [1m------------------------------------------------------------------------[m
[INFO] Total time:  10.730 s
[INFO] Finished at: 2025-09-30T12:09:46+05:30
[INFO] [1m------------------------------------------------------------------------[m