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";
}
}
測試 Web 層
本指南將引導您完成建立 Spring 應用程式並使用 JUnit 進行測試的過程。
您將構建什麼
您將構建一個簡單的 Spring 應用程式並使用 JUnit 對其進行測試。您可能已經知道如何編寫和執行應用程式中各個類的單元測試,因此,在本指南中,我們將重點介紹如何使用 Spring Test 和 Spring Boot 功能來測試 Spring 與您的程式碼之間的互動。您將從一個簡單的測試開始,該測試驗證應用程式上下文是否成功載入,然後繼續使用 Spring 的 MockMvc 僅測試 Web 層。
你需要什麼
-
大約 15 分鐘
-
一個喜歡的文字編輯器或 IDE
-
Java 1.8 或更高版本
-
您還可以將程式碼直接匯入到您的 IDE 中
如何完成本指南
與大多數 Spring 入門指南一樣,您可以從頭開始並完成每個步驟,也可以跳過您已熟悉的基本設定步驟。無論哪種方式,您最終都會得到可工作的程式碼。
要從頭開始,請轉到從 Spring Initializr 開始。
要跳過基礎知識,請執行以下操作
-
下載並解壓本指南的源倉庫,或者使用 Git 克隆它:
git clone https://github.com/spring-guides/gs-testing-web.git -
cd 進入
gs-testing-web/initial -
跳到建立簡單應用程式。
完成時,您可以對照 gs-testing-web/complete 中的程式碼檢查您的結果。
從 Spring Initializr 開始
您可以使用這個預初始化專案,然後點選生成下載 ZIP 檔案。該專案已配置為符合本教程中的示例。
手動初始化專案
-
導航到 https://start.spring.io。此服務會為您拉取應用程式所需的所有依賴項,併為您完成大部分設定。
-
選擇 Gradle 或 Maven 以及您想要使用的語言。
-
點選 Dependencies 並選擇 Spring Web。
-
單擊生成。
-
下載生成的 ZIP 檔案,這是一個已根據您的選擇配置好的 Web 應用程式存檔。
| 如果您的 IDE 集成了 Spring Initializr,您可以從 IDE 中完成此過程。 |
| 您還可以從 GitHub fork 專案,並在您的 IDE 或其他編輯器中開啟它。 |
建立簡單應用程式
為您的 Spring 應用程式建立一個新的控制器。以下列表展示瞭如何操作
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 建立的應用程式類
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);
}
}
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.gradle 或 pom.xml) 中新增必要的依賴項。 |
您可以做的第一件事是編寫一個簡單的健全性檢查測試,如果應用程式上下文無法啟動,該測試將失敗。以下列表展示瞭如何操作
package com.example.testingweb;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TestingWebApplicationTests {
@Test
void contextLoads() {
}
}
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),它應該會透過。為了讓您相信上下文正在建立您的控制器,您可以新增一個斷言,如下例所示
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();
}
}
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 請求並斷言響應。以下列表顯示瞭如何操作
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");
}
}
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 註解來要求將其注入。以下列表顯示瞭如何操作
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")));
}
}
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 層,如下面的列表所示
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")));
}
}
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 很簡單,沒有依賴。我們可以透過引入一個額外的元件來儲存問候語(可能是在一個新的控制器中)使其更真實。以下示例展示瞭如何操作
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();
}
}
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()
}
然後建立一個問候服務,如下所示
package com.example.testingweb;
import org.springframework.stereotype.Service;
@Service
public class GreetingService {
public String greet() {
return "Hello, World";
}
}
package com.example.testingweb
import org.springframework.stereotype.Service
@Service
class GreetingService {
fun greet(): String = "Hello, World"
}
Spring 會自動將服務依賴注入到控制器中(因為建構函式簽名)。以下列表顯示瞭如何使用 @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.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")));
}
}
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 層並載入一個特殊的應用程式上下文。