使用Spring Boot、Kotlin和OpenFeign實現型別安全API測試
有多種方法可以測試你的 Spring Boot 應用程式的 API,雖然啟動時間比MockMvc它稍長,但我更喜歡這種OpenFeign方法。
您可以在我的Github 頁面上找到所有 4 種方法的完整示例程式碼。
1. 在 Spring Boot 應用程式中使用 MockMvc
為了更接近現實生活場景,Spring 提供了MockMvc. 無需啟動成熟的 Web 伺服器,它就可以讓您訪問幾乎任何 HTTP API(GET、POST、HEAD...),並且您還可以使用匹配器來檢查您的控制器是否返回預期的響應:
import org.junit.jupiter.api.Assertions.assertEquals 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.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @SpringBootTest @AutoConfigureMockMvc class MockMvcTest( @Autowired private val mockMvc: MockMvc ) { @Test fun helloWorld() { val result = mockMvc.perform(get("/hello-world")) .andExpect(status().isOk) .andExpect(jsonPath("$.message").value("Hello World")) .andReturn() assertEquals( MediaType.APPLICATION_JSON_VALUE, result.response.contentType ) } } ` |
這裡的問題是,MockMvc並不是在執行時執行的相同程式碼。它很接近,但並不一樣,而且在有些情況下它的行為是不同的。最重要的是,你不能在這裡透過電線做真正的HTTP請求,你沒有完整的錯誤響應處理(當使用重定向時),以及更多。
MockMvc還有一個缺點。它需要大量的冗餘程式碼,因為你必須手動輸入所有的路由和欄位名(當反序列化JSON時),與你的應用程式碼同步。當你的程式碼庫增長和你的API進化時,這可能是很麻煩的。
2.使用TestRestTemplate
取自Spring官方文件的例子是,我們在真實環境中啟動伺服器(使用WebEnvironment.RANDOM_PORT而不是預設的WebEnvironment.MOCK),然後使用TestRestTemplate對我們的伺服器執行真實的HTTP
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.web.server.LocalServerPort @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class TestRestTemplateTest( @LocalServerPort private val localServerPort: Int, @Autowired private val restTemplate: TestRestTemplate ) { @Test fun helloWorld() { val url = "http://localhost:$localServerPort/hello-world" assertThat(restTemplate .getForObject(url, Message::class.java).message) .isEqualTo("Hello World") } } |
已經好多了,不再有模擬環境了,我們已經接近於一個類似生產的場景。
但是,我們仍然需要使用TestRestTemplate手動維護所有的路由,這也是下一個例子中的情況。
3.使用RestAssured
RestAssured是一個很棒的庫,可以針對 REST API 建立自動化測試。設定與方法完全相同TestRestTemplate:
import io.restassured.RestAssured import io.restassured.RestAssured.given import org.assertj.core.api.Assertions.assertThat import org.hamcrest.CoreMatchers.equalTo import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.web.server.LocalServerPort import org.springframework.http.MediaType @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class RestAssuredTest( @LocalServerPort private val localServerPort: Int ) { @BeforeEach fun setup() { RestAssured.port = localServerPort } @Test fun helloWorld() { given().get("/hello-world").then() .statusCode(200) .assertThat() .contentType(MediaType.APPLICATION_JSON_VALUE) .body("message", equalTo("Hello World")) } @Test fun helloWorldMapping() { assertThat(given().get("/hello-world").`as`(Message::class.java).message) .isEqualTo("Hello World") } } |
我在那裡新增了兩個測試,一個沒有物件對映,一個使用Jackson自動對映到我們的Message物件 - 所以你有類似TestRestTemplate的可能性。
請注意,RestAssured的大部分內容是用Groovy編寫的,這意味著執行時間會稍慢一些。不管怎麼說,我確實比TestRestTemplate更喜歡這種語法,它使程式碼更易讀。
總之,儘管我們透過HTTP與我們的伺服器進行通訊,我們仍然需要手動維護我們測試案例中的所有路由,並使它們與伺服器保持同步。
幸運的是,也有一種方法可以解決這個問題,這就把我們引向了最終的解決方案。
4.使用宣告式Feign客戶端進行型別安全的API測試
OpenFeign的宣告式REST客戶端允許我們將路由和MVC對映資訊儲存在一個地方,並在我們的測試案例中重複使用所有這些資訊。
在這之前,我們需要重構我們的伺服器程式碼,並從HelloController中提取一個介面,其中包含所有SpringMVC註釋,如@GetMapping,@PostMapping等。
interface HelloApi { @GetMapping("/hello-world") fun helloWorld(): Message } @RestController class HelloController : HelloApi { override fun helloWorld(): Message { return Message(message = "Hello World") } } |
現在,在 import 之後org.springframework.cloud:spring-cloud-starter-openfeign,我們可以編寫以下測試:
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.web.server.LocalServerPort import org.springframework.cloud.openfeign.FeignClientBuilder import org.springframework.context.ApplicationContext @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class OpenFeignIntegrationTest( @LocalServerPort private val localServerPort: Int, @Autowired private val applicationContext: ApplicationContext ) { private val helloApi = FeignTestClientFactory.createClientApi(HelloApi::class.java, localServerPort, applicationContext) @Test fun helloWorld() { assertThat(helloApi.helloWorld().message).isEqualTo("Hello World") } } object FeignTestClientFactory { fun <T> createClientApi(apiClass: Class<T>, port: Int, clientContext: ApplicationContext): T { return FeignClientBuilder(clientContext) .forType(apiClass, apiClass.canonicalName) .url("http://localhost:$port") .build() } } |
我在這裡提取了一個小的輔助類FeignTestClientFactory,以便更自如地使用FeignClientBuilder - 你可以在你的測試用例中重複使用這個工具。
測試用例本身仍然很短。
- 我們再次使用WebEnvironment.RANDOM_PORT
- @LocalServerPort是由Spring Boot注入的。
- 基於我們的新介面HelloApi建立一個宣告性的Feign客戶端。OpenFeign讀取我們的@GetMapping註解,包括所有路由資訊,並在HelloApi的動態代理後面為我們建立一個HTTP客戶端。
- 這意味著我們現在可以呼叫HelloApi的所有介面方法,但我們不是直接呼叫我們的控制器(如第一個例子),而是做真正的HTTP請求,像其他客戶端一樣訪問我們的伺服器。
現在我們可以對Spring Boot伺服器進行完全型別安全和重構安全的API測試。
- 路由的定義只有一個地方。HelloApi。
- 如果在API中新增了新的方法,或者更新了現有的方法,你可以在測試用例中立即獲得這些變化。
- 在你的IDE中,你有完整的重構支援。
- 你也可以用你的IDE找到所有訪問某個API的測試(透過搜尋HelloApi的所有用法),這對大程式碼庫特別有幫助。
相關文章
- 使用Kotlin (Spring Boot) + MockMVC + DatabaseRider輕鬆實現API整合測試KotlinSpring BootMockMVCDatabaseIDEAPI
- Spring Boot 構建 Restful API 和測試Spring BootRESTAPI
- 如何使用Spring Boot,Spring Data和H2 DB實現REST APISpring BootRESTAPI
- 如何在Spring Boot中實現整合測試?Spring Boot
- 使用Spring Boot REST API進行測試驅動開發Spring BootRESTAPI
- Spring、Spring Boot和TestNG測試指南 – 共享測試配置Spring Boot
- 使用 Spring Boot 和 @SpringBootTest 進行測試Spring Boot
- Spring、Spring Boot和TestNG測試指南 – 使用Spring Testing工具Spring Boot
- 使用Spring Boot和Kafka Streams實現CQRSSpring BootKafka
- 測試boot庫下I/O模型型別boot模型型別
- 使用 Spring Boot 和 @WebMvcTest 測試 MVC Web ControllerSpring BootWebMVCController
- Spring Boot乾貨系列:(十二)Spring Boot使用單元測試Spring Boot
- 使用 Postman 實現 API 自動化測試PostmanAPI
- Spring Boot使用過濾器和攔截器分別實現REST介面簡易安全認證Spring Boot過濾器REST
- spring boot 整合測試Spring Boot
- Spring Boot(七):spring boot測試介紹Spring Boot
- 使用 Spring Boot 進行單元測試Spring Boot
- 安全測試和滲透測試的區別
- Spring Boot中實現乾淨API響應Spring BootAPI
- Spring Boot(十二):Spring Boot 如何測試打包部署Spring Boot
- 使用 Spring Boot 3.2 和 CRaC 實現更快啟動Spring Boot
- 使用Spring Boot實現模組化Spring Boot
- Spring Boot單元和整合測試概述 | rieckpilSpring BootKPI
- Spring Boot 單元測試Spring Boot
- Spring Boot 中測試 CORSSpring BootCORS
- Spring、Spring Boot和TestNG測試指南 – 整合測試中用Docker建立資料庫Spring BootDocker資料庫
- Spring、Spring Boot和TestNG測試指南 – @TestConfigurationSpring Boot
- 在 Kotlin 中“實現”trait/型別類KotlinAI型別
- 使用混沌候攻擊測試Spring Boot應用Spring Boot
- 使用Spring Boot實現的GraphQL示例Spring Boot
- 使用Spring Boot實現事務管理Spring Boot
- 021-Spring Boot 測試Spring Boot
- Spring、Spring Boot和TestNG測試指南 – @OverrideAutoConfigurationSpring BootIdea
- Spring API 的 CORS 測試SpringAPICORS
- 在Spring Boot中實現API閘道器與路由Spring BootAPI路由
- (譯)使用Spring Boot和Axon實現CQRS&Event SourcingSpring Boot
- 使用 Spring Security JWT 令牌簽名實現 REST API 安全性SpringJWTRESTAPI
- 實戰Spring Boot 2.0系列(二) - 全域性異常處理和測試Spring Boot