測試 Web 層

本指南將引導您完成建立 Spring 應用程式並使用 JUnit 進行測試的過程。

您將構建什麼

您將構建一個簡單的 Spring 應用程式並使用 JUnit 進行測試。您可能已經知道如何在應用程式中編寫和執行單個類的單元測試,因此,對於本指南,我們將重點介紹如何使用 Spring Test 和 Spring Boot 的特性來測試 Spring 與您的程式碼之間的互動。您將從一個簡單的測試開始,該測試用於驗證應用程式上下文是否成功載入,然後繼續使用 Spring 的 MockMvc 只測試 Web 層。

您需要什麼

如何完成本指南

與大多數 Spring 入門指南一樣,您可以從頭開始完成每個步驟,也可以跳過您已經熟悉的基本設定步驟。無論哪種方式,您最終都會得到可工作的程式碼。

從頭開始,請轉到從 Spring Initializr 開始

跳過基礎部分,請執行以下操作

完成時,您可以對照 gs-testing-web/complete 中的程式碼檢查您的結果。

從 Spring Initializr 開始

您可以使用這個預初始化的專案,然後點選 Generate 下載一個 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-webmvc 時,Spring Boot 會自動新增它。

  • @ComponentScan:告訴 Spring 在您的帶註解的 TestingWebApplication 類所在的包 (com.example.testingweb) 中查詢其他元件、配置和服務,從而找到 com.example.testingweb.HelloController

main() 方法使用 Spring Boot 的 SpringApplication.run() 方法來啟動應用程式。您注意到沒有一行 XML 程式碼嗎?也沒有 web.xml 檔案。這個 Web 應用程式是 100% 純 Java 的,您無需處理任何底層或基礎設施的配置。Spring Boot 會為您處理所有這些。

日誌輸出會顯示出來。服務應該會在幾秒鐘內啟動並執行。

測試應用程式

現在應用程式正在執行,您可以測試它了。您可以在 https://: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://:" + 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 的 mock(如果您不這樣做,應用程式上下文將無法啟動),然後使用 Mockito 設定其期望。

總結

恭喜!您已經開發了一個 Spring 應用程式,並使用 JUnit 和 Spring MockMvc 對其進行了測試,還使用了 Spring Boot 來隔離 Web 層並載入一個特殊的應用程式上下文。

另請參閱

以下指南可能也有幫助

想撰寫新指南或貢獻現有指南?請檢視我們的貢獻指南

所有指南的程式碼均採用 ASLv2 許可證釋出,文字內容採用 署名-禁止演繹 (Attribution, NoDerivatives) 知識共享許可證

獲取程式碼