测试 Web 层

本指南带你创建一个 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 文件。手动初始化时:

  1. 打开 https://start.spring.io,该服务会拉取所需依赖,并完成大部分设置。
  2. 选择 Gradle 或 Maven,以及使用的语言。
  3. 点击 Dependencies,选择 Spring Web 与 HTTP Client。
  4. 点击 Generate。
  5. 下载 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 层、加载专门的应用上下文。

另请参阅

来源:Spring Getting Started:Testing the Web Layer。Spring 指南代码采用 Apache License 2.0,文字采用 CC BY-ND(版本以原页链接为准)。本篇汉化依据另行取得的转载授权;CC BY-ND 本身不授予公开分发翻译的权利。保留来源、全部 Java/Kotlin 代码与必要许可,不声称原创或测试已运行。Copyright © 2005–2026 Broadcom。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容