測試 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 許可,文字內容採用署名-禁止演繹知識共享許可

獲取程式碼