测试 Web 层

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

您将构建什么

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

您需要什么

如何完成本指南

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

从头开始,请继续执行 从 Spring Initializr 开始

跳过基础知识,请执行以下操作

完成后,您可以将您的结果与 gs-testing-web/complete 中的代码进行比较。

从 Spring Initializr 开始

您可以使用此 预初始化项目 并单击“生成”以下载 ZIP 文件。此项目配置为适合本教程中的示例。

要手动初始化项目

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

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

  3. 单击依赖项并选择Spring Web

  4. 单击生成

  5. 下载生成的 ZIP 文件,该文件是使用您的选择配置的 Web 应用程序的存档。

如果您的 IDE 集成了 Spring Initializr,则可以从您的 IDE 中完成此过程。
您也可以从 Github 分叉项目并在您的 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 Boot 在类路径上看到 spring-webmvc 时,它会自动添加它。

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

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

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

测试应用程序

现在应用程序已运行,您可以对其进行测试。您可以在 https://127.0.0.1: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("https://127.0.0.1:" + 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 的模拟(如果您不这样做,应用程序上下文将无法启动),并使用 Mockito 设置其期望。

总结

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

参见

以下指南可能对您有所帮助

想要编写新的指南或为现有指南做出贡献?请查看我们的 贡献指南

所有指南均以 ASLv2 许可证发布代码,并以 署名-非衍生作品创作共用许可证 发布文字。

获取代码