測試 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 分叉專案,並在您的 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 會為您處理所有這些。

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

測試應用程式

現在應用程式正在執行,您可以對其進行測試。 您可以在 https://127.0.0.1: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://127.0.0.1:" + 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 層並載入特殊的應用程式內容。

另請參閱

下列指南也可能很有幫助

想要編寫新的指南或對現有指南做出貢獻嗎? 請查看我們的 貢獻準則

所有指南均以 ASLv2 授權發布程式碼,並以 署名,禁止衍生作品創用 CC 授權發布寫作。

取得程式碼