本指南带你创建一个 Spring 应用,并使用 JUnit 测试它。
你将构建什么
你将构建简单的 Spring 应用,并用 JUnit 进行测试。你可能已经知道如何为应用中的单个类编写和运行单元测试,因此这里主要介绍使用 Spring Test 和 Spring Boot 的功能,测试 Spring 与你的代码之间的交互。先检查应用上下文能否成功加载,再测试 Web 层。原指南介绍将这一过程与 Spring 的 MockMvc 联系起来;当前正文示例则使用 RestTestClient。
需要准备什么
- 大约15分钟。
- 你喜欢的文本编辑器或 IDE。
- Java 17 或更高版本(原页所列前提,见下方版本提示)。
- Gradle 7.5+ 或 Maven 3.5+。
也可以将代码直接导入 Spring Tool Suite、IntelliJ IDEA 或 VSCode。
如何完成本指南
和其他 Spring 入门指南一样,你可以从头完成每一步,也可以跳过已熟悉的基础设置,最终都能得到可用代码。从头开始时,请继续阅读下一节。
想跳过基础部分,可以下载并解压源代码仓库,或通过 Git 克隆:
git clone https://github.com/spring-guides/gs-testing-web.git
进入 gs-testing-web/initial,然后跳到“创建简单应用”。完成后,可与 gs-testing-web/complete 中的代码比较。
从 Spring Initializr 开始
可以使用原页提供的预配置项目入口,点击 Generate 下载适合本教程的 ZIP 文件。手动初始化时:
- 打开 https://start.spring.io,该服务会拉取所需依赖,并完成大部分设置。
- 选择 Gradle 或 Maven,以及使用的语言。
- 点击 Dependencies,选择 Spring Web 与 HTTP Client。
- 点击 Generate。
- 下载 ZIP,其中是按照你的选择配置的 Web 应用。
如果 IDE 集成了 Spring Initializr,也可以直接在 IDE 中完成。或者在 GitHub fork 项目,再用 IDE 或其他编辑器打开。
创建简单应用
为应用创建一个控制器:
Java
package com.example.testingweb;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@GetMapping("/")
public String greeting() {
return "Hello, World";
}
}
Kotlin
package com.example.testingweb
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class HomeController {
@GetMapping("/")
fun greeting(): String = "Hello, World"
}
运行应用
Spring Initializr 会为你创建包含 main() 方法的应用类。本指南无需修改它:
Java
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);
}
}
Kotlin
package com.example.testingweb
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class TestingWebApplication
fun main(args: Array<String>) {
runApplication<TestingWebApplication>(*args)
}
可以直接在 IDE 中运行应用类,或通过 Gradle 命令 ./gradlew :bootRun、Maven 命令 ./mvnw spring-boot:run 运行。
应用运行后,可打开 http://localhost:8080 检查主页。不过,为了在代码变动后仍有把握确认应用正常工作,应该把测试自动化。
测试应用
Spring Boot 默认你会测试应用,因此会在 build.gradle 或 pom.xml 中加入相应依赖。首先编写简单的基本检查:如果应用上下文无法启动,测试就失败。
Java
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TestingWebApplicationTests {
@Test
void contextLoads() {
}
}
Kotlin
package com.example.testingweb
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class TestingWebApplicationTests {
@Test
fun contextLoads() {
}
}
@SpringBootTest 告诉 Spring Boot 查找主配置类,例如标有 @SpringBootApplication 的类,并据此启动应用上下文。可以在 IDE 中运行,或执行 ./mvnw test、./gradlew check,预期测试通过。若要确认上下文确实创建了控制器,还可以加入断言:
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();
}
}
Kotlin
package com.example.testingweb
import 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 val controller: HomeController) {
@Test
fun contextLoads() {
assertThat(controller).isNotNull()
}
}
Spring 会处理 @Autowired,在测试方法运行前注入控制器。示例用 AssertJ 的 assertThat() 等方法表达断言。
基本检查很有用,但还应该验证应用行为。可以像生产环境一样启动应用并监听连接,再发送 HTTP 请求,断言响应:
Java
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.test.web.servlet.client.RestTestClient;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class HttpRequestTest {
@LocalServerPort
private int port;
@Autowired
private RestTestClient restTestClient;
@Test
void greetingShouldReturnDefaultMessage() {
restTestClient.get()
.uri("http://localhost:%d/".formatted(port))
.exchange()
.expectBody(String.class)
.isEqualTo("Hello, World");
}
}
Kotlin
package com.example.testingweb
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class HttpRequestTest(@Autowired private val restTestClient: RestTestClient) {
@LocalServerPort
private var port: Int = 0
@Test
fun greetingShouldReturnDefaultMessage() {
// Import Kotlin .expectBody() extension that allows using reified type parameters
restTestClient.get()
.uri("http://localhost:$port/")
.exchange()
.expectBody<String>()
.isEqualTo("Hello, World")
}
}
webEnvironment=RANDOM_PORT 会使用随机端口启动服务器,避免测试环境中的端口冲突;@LocalServerPort 注入该端口。由于使用了 @AutoConfigureRestTestClient,Spring Boot 会自动提供 RestTestClient,随后通过 @Autowired 注入字段。
不启动服务器的测试
另一个实用方法是完全不启动服务器,而测试下一层:由 Spring 处理传入 HTTP 请求,并交给控制器。这样几乎会经过整个技术栈,代码的调用方式与处理真实 HTTP 请求一致,又无需承担启动服务器的成本。
复用上面的 RestTestClient 测试,保留 @SpringBootTest 默认设置,启动模拟服务器环境:
Java
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.client.RestTestClient;
@SpringBootTest
@AutoConfigureRestTestClient
class TestingWebApplicationTest {
@Autowired
private RestTestClient restTestClient;
@Test
void greetingShouldReturnDefaultMessage() {
restTestClient.get().uri("/")
.exchange()
.expectBody(String.class)
.isEqualTo("Hello, World");
}
}
Kotlin
package com.example.testingweb
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody
@SpringBootTest
@AutoConfigureRestTestClient
class TestingWebApplicationTest(@Autowired private val restTestClient: RestTestClient) {
@Test
fun greetingShouldReturnDefaultMessage() {
// Import Kotlin .expectBody() extension that allows using reified type parameters
restTestClient.get().uri("/")
.exchange()
.expectBody<String>()
.isEqualTo("Hello, World")
}
}
此测试启动完整的 Spring 应用上下文,但不启动服务器。还可以用 @WebMvcTest 将范围缩小到 Web 层:
Java
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.client.RestTestClient;
@WebMvcTest(HomeController.class)
@AutoConfigureRestTestClient
class WebLayerTest {
@Autowired
private RestTestClient restTestClient;
@Test
void greetingShouldReturnDefaultMessage() {
restTestClient.get().uri("/")
.exchange()
.expectBody(String.class)
.isEqualTo("Hello, World");
}
}
Kotlin
package com.example.testingweb
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody
@WebMvcTest(HomeController::class)
@AutoConfigureRestTestClient
class WebLayerTest(@Autowired private val restTestClient: RestTestClient) {
@Test
fun greetingShouldReturnDefaultMessage() {
// Import Kotlin .expectBody() extension that allows using reified type parameters
restTestClient.get()
.uri("/")
.exchange()
.expectBody<String>()
.isEqualTo("Hello, World")
}
}
断言与前例相同,但 Spring Boot 只实例化 Web 层,不加载整个上下文。如果应用有多个控制器,还可指定只实例化一个,例如 @WebMvcTest(HomeController.class)。
引入业务服务并进行模拟
目前 HomeController 很简单,没有依赖。为使它更贴近实际,可以引入额外组件保存问候语,例如新增一个控制器:
Java
package com.example.testingweb;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private final GreetingService service;
public GreetingController(GreetingService service) {
this.service = service;
}
@GetMapping("/greeting")
public String greeting() {
return service.greet();
}
}
Kotlin
package com.example.testingweb
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class GreetingController(private val service: GreetingService) {
@GetMapping("/greeting")
fun greeting(): String = service.greet()
}
然后创建问候服务:
Java
package com.example.testingweb;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String greet() {
return "Hello, World";
}
}
Kotlin
package com.example.testingweb
import org.springframework.stereotype.Service
@Service
class GreetingService {
fun greet(): String = "Hello, World"
}
Spring 会根据构造函数签名自动将服务依赖注入控制器。以下示例使用 @WebMvcTest 测试该控制器:
Java
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.client.RestTestClient;
import static org.mockito.Mockito.when;
@WebMvcTest(GreetingController.class)
@AutoConfigureRestTestClient
class WebMockTest {
@Autowired
private RestTestClient restTestClient;
@MockitoBean
private GreetingService service;
@Test
void greetingShouldReturnMessageFromService() throws Exception {
when(service.greet()).thenReturn("Hello, Mock");
restTestClient.get().uri("/greeting")
.exchange()
.expectBody(String.class)
.isEqualTo("Hello, Mock");
}
}
Kotlin
package com.example.testingweb
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.web.servlet.client.RestTestClient
import org.springframework.test.web.servlet.client.expectBody
@WebMvcTest(GreetingController::class)
@AutoConfigureRestTestClient
class WebMockTest(@Autowired private val restTestClient: RestTestClient) {
@MockitoBean
private lateinit var service: GreetingService
@Test
fun greetingShouldReturnMessageFromService() {
`when`(service.greet()).thenReturn("Hello, Mock")
// Import Kotlin .expectBody() extension that allows using reified type parameters
restTestClient.get()
.uri("/greeting")
.exchange()
.expectBody<String>()
.isEqualTo("Hello, Mock")
}
}
@MockitoBean 创建并注入 GreetingService 的模拟对象;否则应用上下文无法启动。再通过 Mockito 设置模拟行为的预期。
小结
完成这些步骤后,你就创建了一个 Spring 应用,用 JUnit 与 Spring 的 Web 测试工具测试它,并使用 Spring Boot 隔离 Web 层、加载专门的应用上下文。











暂无评论内容