22-01-25
banq
有多种方法可以测试你的 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的所有用法),这对大代码库特别有帮助。