测试 Web 层

本指南将引导您完成创建 Spring 应用程序并使用 JUnit 进行测试的过程。

您将构建什么

您将构建一个简单的 Spring 应用程序并使用 JUnit 进行测试。您可能已经知道如何在应用程序中编写和运行单个类的单元测试,因此,对于本指南,我们将重点介绍如何使用 Spring Test 和 Spring Boot 的特性来测试 Spring 与您的代码之间的交互。您将从一个简单的测试开始,该测试用于验证应用程序上下文是否成功加载,然后继续使用 Spring 的 MockMvc 只测试 Web 层。

您需要什么

如何完成本指南

与大多数 Spring 入门指南一样,您可以从头开始完成每个步骤,也可以跳过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到可工作的代码。

从头开始,请转到从 Spring Initializr 开始

跳过基础部分,请执行以下操作

完成时,您可以对照 gs-testing-web/complete 中的代码检查您的结果。

从 Spring Initializr 开始

您可以使用这个预初始化的项目,然后点击 Generate 下载一个 ZIP 文件。该项目已配置为与本教程中的示例相符。

手动初始化项目

  1. 导航到https://start.spring.io。该服务会拉取您的应用程序所需的所有依赖项,并为您完成大部分设置。

  2. 选择 Gradle 或 Maven 以及您想使用的语言。本指南假设您选择了 Java。

  3. 点击 Dependencies 并选择 Spring Web

  4. 点击 Generate

  5. 下载生成的 ZIP 文件,这是一个已根据您的选择配置好的 Web 应用程序存档。

如果您的 IDE 集成了 Spring Initializr,您可以直接在 IDE 中完成此过程。
您还可以从 Github fork 该项目,并在您的 IDE 或其他编辑器中打开它。

创建简单的应用程序

为您的 Spring 应用程序创建一个新的控制器。以下列表(来自 src/main/java/com/example/testingweb/HomeController.java)展示了如何操作

package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HomeController {

	@RequestMapping("/")
	public @ResponseBody String greeting() {
		return "Hello, World";
	}

}
上述示例没有区分 GETPUTPOST 等。默认情况下,@RequestMapping 映射所有 HTTP 操作。您可以使用 @GetMapping@RequestMapping(method=GET) 来缩小此映射范围。

运行应用程序

Spring Initializr 会为您创建一个应用程序类(一个包含 main() 方法的类)。对于本指南,您无需修改此类。以下列表(来自 src/main/java/com/example/testingweb/TestingWebApplication.java)展示了 Spring Initializr 创建的应用程序类

package com.example.testingweb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TestingWebApplication {

	public static void main(String[] args) {
		SpringApplication.run(TestingWebApplication.class, args);
	}
}

@SpringBootApplication 是一个便捷注解,包含了以下所有注解

  • @Configuration:将类标记为应用程序上下文的 Bean 定义源。

  • @EnableAutoConfiguration:告诉 Spring Boot 根据类路径设置、其他 Bean 和各种属性设置开始添加 Bean。

  • @EnableWebMvc:将应用程序标记为 Web 应用程序并激活关键行为,例如设置 DispatcherServlet。当在类路径中看到 spring-webmvc 时,Spring Boot 会自动添加它。

  • @ComponentScan:告诉 Spring 在您的带注解的 TestingWebApplication 类所在的包 (com.example.testingweb) 中查找其他组件、配置和服务,从而找到 com.example.testingweb.HelloController

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法来启动应用程序。您注意到没有一行 XML 代码吗?也没有 web.xml 文件。这个 Web 应用程序是 100% 纯 Java 的,您无需处理任何底层或基础设施的配置。Spring Boot 会为您处理所有这些。

日志输出会显示出来。服务应该会在几秒钟内启动并运行。

测试应用程序

现在应用程序正在运行,您可以测试它了。您可以在 http://localhost:8080 加载主页。然而,为了让您在进行更改时对应用程序的功能更有信心,您需要自动化测试过程。

Spring Boot 假设您计划测试您的应用程序,因此它会将必要的依赖项添加到您的构建文件(build.gradlepom.xml)中。

您可以做的第一件事是编写一个简单的健全性检查测试,如果应用程序上下文无法启动,该测试将失败。以下列表(来自 src/test/java/com/example/testingweb/TestingWebApplicationTest.java)展示了如何操作

package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class TestingWebApplicationTests {

	@Test
	void contextLoads() {
	}

}

@SpringBootTest 注解告诉 Spring Boot 查找主配置类(例如带有 @SpringBootApplication 的类),并使用它来启动 Spring 应用程序上下文。您可以在 IDE 中或命令行中运行此测试(通过运行 ./mvnw test./gradlew test),并且它应该会通过。为了让您确信上下文正在创建您的控制器,您可以添加一个断言,如下例所示(来自 src/test/java/com/example/testingweb/SmokeTest.java

package com.example.testingweb;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SmokeTest {

	@Autowired
	private HomeController controller;

	@Test
	void contextLoads() throws Exception {
		assertThat(controller).isNotNull();
	}
}

Spring 会解析 @Autowired 注解,并在测试方法运行之前注入控制器。我们使用 AssertJ(它提供了 assertThat() 和其他方法)来表达测试断言。

Spring Test 支持的一个很好的特性是应用程序上下文会在测试之间被缓存。这样,如果一个测试用例中有多个方法,或者有多个测试用例使用相同的配置,它们只需承担一次启动应用程序的成本。您可以使用 @DirtiesContext 注解来控制缓存。

进行健全性检查很好,但您还应该编写一些测试来断言应用程序的行为。为此,您可以启动应用程序并监听连接(就像在生产环境中那样),然后发送 HTTP 请求并断言响应。以下列表(来自 src/test/java/com/example/testingweb/HttpRequestTest.java)展示了如何操作

package com.example.testingweb;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.web.server.LocalServerPort;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class HttpRequestTest {

	@LocalServerPort
	private int port;

	@Autowired
	private TestRestTemplate restTemplate;

	@Test
	void greetingShouldReturnDefaultMessage() throws Exception {
		assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/",
				String.class)).contains("Hello, World");
	}
}

请注意使用 webEnvironment=RANDOM_PORT 以随机端口启动服务器(这有助于避免测试环境中的冲突)以及使用 @LocalServerPort 注入端口。另外,请注意 Spring Boot 已自动为您提供了 TestRestTemplate。您只需为其添加 @Autowired 即可。

另一个有用的方法是根本不启动服务器,而只测试其下面的层,即 Spring 处理传入的 HTTP 请求并将其传递给您的控制器。这样,几乎使用了完整的技术栈,并且您的代码将以与处理真实 HTTP 请求完全相同的方式被调用,但无需启动服务器的成本。为此,请使用 Spring 的 MockMvc,并通过在测试用例上使用 @AutoConfigureMockMvc 注解来请求将其注入。以下列表(来自 src/test/java/com/example/testingweb/TestingWebApplicationTest.java)展示了如何操作

package com.example.testingweb;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

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.test.web.servlet.MockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class TestingWebApplicationTest {

	@Autowired
	private MockMvc mockMvc;

	@Test
	void shouldReturnDefaultMessage() throws Exception {
		this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, World")));
	}
}

在此测试中,完整的 Spring 应用程序上下文已启动,但不包括服务器。我们可以通过使用 @WebMvcTest 将测试范围缩小到仅 Web 层,如下列表(来自 src/test/java/com/example/testingweb/WebLayerTest.java)所示

@WebMvcTest
include::complete/src/test/java/com/example/testingweb/WebLayerTest.java

测试断言与前一个示例相同。然而,在此测试中,Spring Boot 仅实例化 Web 层而不是整个上下文。在一个包含多个控制器的应用程序中,您甚至可以通过使用例如 @WebMvcTest(HomeController.class) 来指定只实例化其中一个。

到目前为止,我们的 HomeController 很简单,并且没有依赖项。我们可以通过引入一个额外的组件来存储问候语(可能在一个新的控制器中)使其更具真实性。以下示例(来自 src/main/java/com/example/testingweb/GreetingController.java)展示了如何操作

package com.example.testingweb;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
public class GreetingController {

	private final GreetingService service;

	public GreetingController(GreetingService service) {
		this.service = service;
	}

	@RequestMapping("/greeting")
	public @ResponseBody String greeting() {
		return service.greet();
	}

}

然后创建一个问候语服务,如下列表(来自 src/main/java/com/example/testingweb/GreetingService.java)所示

package com.example.testingweb;

import org.springframework.stereotype.Service;

@Service
public class GreetingService {
	public String greet() {
		return "Hello, World";
	}
}

Spring 会自动将服务依赖项注入到控制器中(由于构造函数签名)。以下列表(来自 src/test/java/com/example/testingweb/WebMockTest.java)展示了如何使用 @WebMvcTest 测试此控制器

package com.example.testingweb;

import static org.hamcrest.Matchers.containsString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class WebMockTest {

	@Autowired
	private MockMvc mockMvc;

	@MockBean
	private GreetingService service;

	@Test
	void greetingShouldReturnMessageFromService() throws Exception {
		when(service.greet()).thenReturn("Hello, Mock");
		this.mockMvc.perform(get("/greeting")).andDo(print()).andExpect(status().isOk())
				.andExpect(content().string(containsString("Hello, Mock")));
	}
}

我们使用 @MockBean 创建并注入 GreetingService 的 mock(如果您不这样做,应用程序上下文将无法启动),然后使用 Mockito 设置其期望。

总结

恭喜!您已经开发了一个 Spring 应用程序,并使用 JUnit 和 Spring MockMvc 对其进行了测试,还使用了 Spring Boot 来隔离 Web 层并加载一个特殊的应用程序上下文。

另请参阅

以下指南可能也有帮助

想撰写新指南或贡献现有指南?请查看我们的贡献指南

所有指南的代码均采用 ASLv2 许可证发布,文字内容采用 署名-禁止演绎 (Attribution, NoDerivatives) 知识共享许可证

获取代码