Spring Boot - Rest 控制器单元测试



Spring Boot 提供了一种简单的方法来为 Rest Controller 文件编写单元测试。借助 SpringJUnit4ClassRunner 和 MockMvc,我们可以创建一个 Web 应用程序上下文来为 Rest Controller 文件编写单元测试。

单元测试应该写在 src/test/java 目录下,编写测试的类路径资源应该放在 src/test/resources 目录下。

为了编写单元测试,我们需要在构建配置文件中添加 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 服务。有关构建 RESTful Web 服务的更多信息,请参阅 构建 RESTful Web 服务 章节。

为 REST 控制器编写单元测试

在本节中,让我们看看如何为 REST 控制器编写单元测试。

首先,我们需要创建一个抽象类文件,用于使用 MockMvc 创建 Web 应用程序上下文,并定义 mapToJson() 和 mapFromJson() 方法,以将 Java 对象转换为 JSON 字符串,并将 JSON 字符串转换为 Java 对象。

AbstractTest.java

package com.tutorialspoint.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.tutorialspoint.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.tutorialspoint.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.3.3</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>
   <groupId>com.tutorialspoint</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>

编译和执行

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

对于 Maven,您可以使用以下命令:

mvn clean install 

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

[INFO] Scanning for projects...
[INFO] 
[INFO] [1m----------------------< [0;36mcom.tutorialspoint:demo[0;1m >-----------------------[m
[INFO] [1mBuilding demo 0.0.1-SNAPSHOT[m
[INFO]   from pom.xml
[INFO] [1m--------------------------------[ jar ]---------------------------------[m
...
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.tutorialspoint.demo.[1mDemoApplicationTests[m
[2024-09-25T15:56:58Z] [org.springframework.test.context.support.AnnotationConfigContextLoaderUtils] [main] [83] [INFO ] Could not detect default configuration classes for test class [com.tutorialspoint.demo.DemoApplicationTests]: DemoApplicationTests does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
[2024-09-25T15:56:58Z] [org.springframework.boot.test.context.SpringBootTestContextBootstrapper] [main] [234] [INFO ] Found @SpringBootConfiguration com.tutorialspoint.demo.DemoApplication for test class com.tutorialspoint.demo.DemoApplicationTests

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

 :: Spring Boot ::                (v3.3.3)

[2024-09-25T15:56:59Z] [org.springframework.boot.StartupInfoLogger] [main] [50] [INFO ] Starting DemoApplicationTests using Java 21.0.3 with PID 7200 (started by Tutorialspoint in E:\Dev\demo)
[2024-09-25T15:56:59Z] [org.springframework.boot.SpringApplication] [main] [654] [INFO ] No active profile set, falling back to 1 default profile: "default"
...
[INFO] [1;32mTests run: [0;1;32m1[m, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.972 s -- in com.tutorialspoint.demo.[1mDemoApplicationTests[m
[INFO] Running com.tutorialspoint.demo.[1mProductServiceControllerTest[m

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

 :: Spring Boot ::                (v3.3.3)

[2024-09-25T15:57:04Z] [org.springframework.boot.StartupInfoLogger] [main] [50] [INFO ] Starting ProductServiceControllerTest using Java 21.0.3 with PID 7200 (started by Tutorialspoint in E:\Dev\demo)
[2024-09-25T15:57:04Z] [org.springframework.boot.SpringApplication] [main] [654] [INFO ] No active profile set, falling back to 1 default profile: "default"
[2024-09-25T15:57:04Z] [org.springframework.boot.autoconfigure.web.servlet.WelcomePageHandlerMapping] [main] [59] [INFO ] Adding welcome page template: index
[2024-09-25T15:57:04Z] [org.springframework.boot.StartupInfoLogger] [main] [56] [INFO ] Started ProductServiceControllerTest in 0.79 seconds (process running for 7.632)
[2024-09-25T15:57:04Z] [org.springframework.mock.web.MockServletContext] [main] [437] [INFO ] Initializing Spring TestDispatcherServlet ''
[2024-09-25T15:57:04Z] [org.springframework.web.servlet.FrameworkServlet] [main] [532] [INFO ] Initializing Servlet ''
[2024-09-25T15:57:04Z] [org.springframework.web.servlet.FrameworkServlet] [main] [554] [INFO ] Completed initialization in 1 ms
...
[INFO] [1;32mTests run: [0;1;32m4[m, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.108 s -- in com.tutorialspoint.demo.[1mProductServiceControllerTest[m
[INFO] 
[INFO] Results:
[INFO] 
[INFO] [1;32mTests run: 5, Failures: 0, Errors: 0, Skipped: 0[m
[INFO] 
[INFO] 
[INFO] [1m--- [0;32mjar:3.4.2:jar[m [1m(default-jar)[m @ [36mdemo[0;1m ---[m
[INFO] Building jar: E:\Dev\demo\target\demo-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] [1m--- [0;32mspring-boot:3.3.3:repackage[m [1m(repackage)[m @ [36mdemo[0;1m ---[m
[INFO] Replacing main artifact E:\Dev\demo\target\demo-0.0.1-SNAPSHOT.jar with repackaged archive, adding nested dependencies in BOOT-INF/.
[INFO] The original artifact has been renamed to E:\Dev\demo\target\demo-0.0.1-SNAPSHOT.jar.original
[INFO] 
[INFO] [1m--- [0;32minstall:3.1.3:install[m [1m(default-install)[m @ [36mdemo[0;1m ---[m
[INFO] Installing E:\Dev\demo\pom.xml to C:\Users\Tutorialspoint\.m2\repository\com\tutorialspoint\demo\0.0.1-SNAPSHOT\demo-0.0.1-SNAPSHOT.pom
[INFO] Installing E:\Dev\demo\target\demo-0.0.1-SNAPSHOT.jar to C:\Users\Tutorialspoint\.m2\repository\com\tutorialspoint\demo\0.0.1-SNAPSHOT\demo-0.0.1-SNAPSHOT.jar
[INFO] [1m------------------------------------------------------------------------[m
[INFO] [1;32mBUILD SUCCESS[m
[INFO] [1m------------------------------------------------------------------------[m
[INFO] Total time:  13.467 s
[INFO] Finished at: 2024-09-25T15:57:08+05:30
[INFO] [1m------------------------------------------------------------------------[m

对于 Gradle,您可以使用以下命令:

gradle clean build
广告