Spring Boot - Rest Controller 单元测试
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