使用Spring Boot、Kotlin和OpenFeign實現型別安全API測試

banq發表於2022-01-25

有多種方法可以測試你的 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的所有用法),這對大程式碼庫特別有幫助。

Github page.

相關文章