测试 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 以及您想要使用的语言。

  3. 点击 Dependencies 并选择 Spring Web

  4. 单击生成

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

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

创建简单应用程序

为您的 Spring 应用程序创建一个新的控制器。以下列表展示了如何操作

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() 方法的类)。对于本指南,您无需修改此应用程序类。以下列表显示了 Spring Initializr 创建的应用程序类

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)
}

@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 文件。您不必处理任何管道或基础设施的配置。Spring Boot 会为您处理所有这些。

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

测试应用程序

现在应用程序正在运行,您可以对其进行测试。您可以加载主页 https://:8080。但是,为了让您在进行更改时更有信心应用程序能够正常工作,您需要自动化测试。

Spring Boot 假设您计划测试您的应用程序,因此它会在您的构建文件 (build.gradlepom.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 的类),并使用它来启动 Spring 应用程序上下文。您可以在 IDE 或命令行中运行此测试(通过运行 ./mvnw test./gradlew test),它应该会通过。为了让您相信上下文正在创建您的控制器,您可以添加一个断言,如下例所示

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 lateinit var controller: HomeController

    @Test
    fun contextLoads() {
        assertThat(controller).isNotNull()
    }
}

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

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

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

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.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://:" + port + "/",
				String.class)).contains("Hello, World");
	}
}
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
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.client.getForObject
import org.springframework.boot.test.web.server.LocalServerPort

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

    @LocalServerPort
    private var port: Int = 0

    @Autowired
    private lateinit var restTemplate: TestRestTemplate

    @Test
    fun greetingShouldReturnDefaultMessage() {
        // Import Kotlin .getForObject() extension that allows using reified type parameters
        assertThat(this.restTemplate.getForObject<String>("https://:$port/"))
            .contains("Hello, World")
    }
}

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

另一种有用的方法是根本不启动服务器,而只测试其下方的层,即 Spring 处理传入的 HTTP 请求并将其交给您的控制器。这样,几乎所有的完整堆栈都被使用了,您的代码将以与处理真实 HTTP 请求完全相同的方式被调用,而无需启动服务器的成本。为此,请使用 Spring 的 MockMvc,并通过在测试用例上使用 @AutoConfigureMockMvc 注解来要求将其注入。以下列表显示了如何操作

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")));
	}
}
Kotlin
package com.example.testingweb

import org.hamcrest.Matchers.containsString
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
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@SpringBootTest
@AutoConfigureMockMvc
class TestingWebApplicationTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

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

在此测试中,启动了完整的 Spring 应用程序上下文,但没有启动服务器。我们可以通过使用 @WebMvcTest 将测试范围缩小到仅 Web 层,如下面的列表所示

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.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(HomeController.class)
class WebLayerTest {

	@Autowired
	private MockMvc mockMvc;

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

import org.hamcrest.Matchers.containsString
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.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@WebMvcTest(HomeController::class)
class WebLayerTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @Test
    fun shouldReturnDefaultMessage() {
        mockMvc.perform(get("/"))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(content().string(containsString("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 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.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(GreetingController.class)
class WebMockTest {

	@Autowired
	private MockMvc mockMvc;

	@MockitoBean
	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")));
	}
}
Kotlin
package com.example.testingweb

import org.hamcrest.Matchers.containsString
import org.junit.jupiter.api.Test
import org.mockito.Mockito.`when`
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.context.bean.override.mockito.MockitoBean
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@WebMvcTest(GreetingController::class)
class WebMockTest {

    @Autowired
    private lateinit var mockMvc: MockMvc

    @MockitoBean
    private lateinit var service: GreetingService

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

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

总结

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

另请参阅

以下指南也可能有所帮助

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

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

获取代码