Spring Boot 与 Docker
观察 GraphQL 的实际运行

本指南将引导您完成创建一个 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. 点击 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 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 为您处理了所有这些。

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

测试应用程序

现在应用程序已经运行起来了,您可以对其进行测试。您可以在 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 的模拟对象(如果不这样做,应用程序上下文将无法启动),并使用 Mockito 设置其预期行为。

总结

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

另请参阅

以下指南可能也会有所帮助:

本页目录